From 1b6a54c55d64ee1f47d3276c15450cbba3ff838d Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Fri, 15 Feb 2013 19:57:05 +0000 Subject: [PATCH 01/71] started on tests for SongShowPlusImport --- openlp/plugins/songs/lib/__init__.py | 2 +- .../openlp_plugins_songs_lib/__init__.py | 0 .../test_songshowplusimport.py | 43 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/functional/openlp_plugins_songs_lib/__init__.py create mode 100644 tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index c7c24533b..4041bb12e 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -167,7 +167,7 @@ class VerseType(object): translate('SongsPlugin.VerseType', 'Intro'), translate('SongsPlugin.VerseType', 'Ending'), translate('SongsPlugin.VerseType', 'Other')] - TranslatedTags = [name[0].lower() for name in TranslatedNames] + TranslatedTags = [unicode(name[0]).lower() for name in TranslatedNames] @staticmethod def translated_tag(verse_tag, default=Other): diff --git a/tests/functional/openlp_plugins_songs_lib/__init__.py b/tests/functional/openlp_plugins_songs_lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py b/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py new file mode 100644 index 000000000..6189c6a7f --- /dev/null +++ b/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py @@ -0,0 +1,43 @@ +""" + Package to test the openlp.plugins.songs.lib package. +""" +import os + +from unittest import TestCase +from mock import MagicMock, patch +from openlp.plugins.songs.lib import songshowplusimport + +TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) + + +class TestSongShowPlusImport(TestCase): + + def default_test(self): + """ + Test the defaults of songshowplusimport + """ + # Given: The songshowplusimport module as imported + + # When: Imported the module should have defaults set + constants = {u'TITLE' : 1, u'AUTHOR' : 2, u'COPYRIGHT' : 3, u'CCLI_NO' : 5, u'VERSE' : 12, u'CHORUS' : 20, + u'BRIDGE' : 24, u'TOPIC' : 29, u'COMMENTS' : 30, u'VERSE_ORDER' : 31, u'SONG_BOOK' : 35, + u'SONG_NUMBER' : 36, u'CUSTOM_VERSE' : 37, u'SongShowPlusImport.otherList' : {}, + u'SongShowPlusImport.otherCount' : 0} + + # Then: The constants should not have changed. + for constant in constants: + value = constants[constant] + self.assertEquals(eval(u'songshowplusimport.%s' % constant), value, + u'%s should be set as %s' % (constant, value)) + + + def do_import_test(self): + mocked_manager = MagicMock() + songshowplusimport.SongImport = MagicMock() + + with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport') as mocked_song_import: + ssp_import_class = songshowplusimport.SongShowPlusImport(mocked_manager) + + songshowplusimport.SongShowPlusImport.importSource = '' + + self.assertEquals(ssp_import_class.SongShowPlusImport().doImport(), False) From ca2b2db6402fbe94e55e8698a00dc193714de7e2 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 17 Feb 2013 19:37:59 +0000 Subject: [PATCH 02/71] fixed duplicates in verse order when adding verses with the same tag. fixed handling of part verses eg (1a, 1b, 1.5, etc) in SongShowPlus importer --- openlp/plugins/songs/lib/songimport.py | 3 ++- .../plugins/songs/lib/songshowplusimport.py | 16 +++++++++----- .../test_songshowplusimport.py | 22 +++++-------------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index f6a84945c..0d563935f 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -260,7 +260,8 @@ 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) + 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 c5bb8832d..8e4957c71 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,13 +45,13 @@ 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__) @@ -183,13 +184,16 @@ class SongShowPlusImport(SongImport): 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] + # 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(u'(\D*)(\d+)', verse_name) + if match is not None: + 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] diff --git a/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py b/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py index 6189c6a7f..77733a1ba 100644 --- a/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py @@ -12,28 +12,16 @@ TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', class TestSongShowPlusImport(TestCase): - def default_test(self): - """ - Test the defaults of songshowplusimport - """ - # Given: The songshowplusimport module as imported +#test do import + # set self.import source to non list type. Do import should return None or False? + # set self.import source to a list of files + # importWizard.progressBar should be set to the number of files in the list + # set self.stop_import_flag to true. Do import should return None or False? - # When: Imported the module should have defaults set - constants = {u'TITLE' : 1, u'AUTHOR' : 2, u'COPYRIGHT' : 3, u'CCLI_NO' : 5, u'VERSE' : 12, u'CHORUS' : 20, - u'BRIDGE' : 24, u'TOPIC' : 29, u'COMMENTS' : 30, u'VERSE_ORDER' : 31, u'SONG_BOOK' : 35, - u'SONG_NUMBER' : 36, u'CUSTOM_VERSE' : 37, u'SongShowPlusImport.otherList' : {}, - u'SongShowPlusImport.otherCount' : 0} - - # Then: The constants should not have changed. - for constant in constants: - value = constants[constant] - self.assertEquals(eval(u'songshowplusimport.%s' % constant), value, - u'%s should be set as %s' % (constant, value)) def do_import_test(self): mocked_manager = MagicMock() - songshowplusimport.SongImport = MagicMock() with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport') as mocked_song_import: ssp_import_class = songshowplusimport.SongShowPlusImport(mocked_manager) From ea86ce905c382cc674a439f3a5e76bf57b679524 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Mon, 18 Feb 2013 17:15:07 +0000 Subject: [PATCH 03/71] Simplified if statment --- openlp/plugins/songs/lib/songshowplusimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index 8e4957c71..c9f9617df 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -187,7 +187,7 @@ class SongShowPlusImport(SongImport): # 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(u'(\D*)(\d+)', verse_name) - if match is not None: + if match: verse_type = match.group(1).strip() verse_number = match.group(2) else: From 40de8647cf36dab6c14c8201777cc3a9df44d9b7 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 28 Feb 2013 21:47:59 +0100 Subject: [PATCH 04/71] renamed general settings to core Fixes: https://launchpad.net/bugs/1133237 --- openlp/core/__init__.py | 10 +-- openlp/core/lib/screen.py | 12 +-- openlp/core/lib/settings.py | 73 +++++++++++++------ openlp/core/ui/firsttimeform.py | 6 +- openlp/core/ui/generaltab.py | 2 +- openlp/core/ui/maindisplay.py | 8 +- openlp/core/ui/mainwindow.py | 2 +- openlp/core/ui/media/mediacontroller.py | 2 +- openlp/core/utils/languagemanager.py | 4 +- openlp/plugins/songs/lib/mediaitem.py | 4 +- .../openlp_core_lib/test_settings.py | 6 +- 11 files changed, 78 insertions(+), 51 deletions(-) diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 3ad0e1348..176fa059a 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -113,10 +113,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'): @@ -128,7 +128,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() @@ -309,7 +309,7 @@ def main(args=None): if application.is_already_running(): sys.exit() # 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/screen.py b/openlp/core/lib/screen.py index 913ad539c..149f07ce1 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/settings.py b/openlp/core/lib/settings.py index be869ade6..e067242dc 100644 --- a/openlp/core/lib/settings.py +++ b/openlp/core/lib/settings.py @@ -116,30 +116,30 @@ 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]', + 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]', # 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/last version test': datetime.datetime.now().date() - datetime.timedelta(days=1), + 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 +304,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', []), # Changed during 2.1.x development. (u'advanced/stylesheet fix', u'', []), (u'bibles/last directory 1', u'bibles/last directory import', []), @@ -314,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/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 188bc3c02..0d749176b 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -120,7 +120,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') @@ -254,7 +254,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): @@ -461,7 +461,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 a20206f9b..af99c3271 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -49,7 +49,7 @@ class GeneralTab(SettingsTab): self.screens = ScreenList() self.iconPath = u':/icon/openlp-logo-16x16.png' generalTranslated = translate('OpenLP.GeneralTab', 'General') - SettingsTab.__init__(self, parent, u'General', generalTranslated) + SettingsTab.__init__(self, parent, u'Core', generalTranslated) def setupUi(self): """ diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 58d832101..41413b503 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -367,7 +367,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) @@ -416,7 +416,7 @@ class MainDisplay(Display): self.footer(serviceItem.foot_text) # if was hidden keep it hidden if self.hideMode and self.isLive and not serviceItem.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.hideMode) @@ -438,7 +438,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");') @@ -462,7 +462,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 14186cf78..0d77da403 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -469,7 +469,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.uiSettingsSection = u'user interface' - self.generalSettingsSection = u'general' + self.generalSettingsSection = u'core' self.advancedSettingsSection = u'advanced' self.shortcutsSettingsSection = u'shortcuts' self.serviceManagerSettingsSection = u'servicemanager' diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 048fb5f4d..60c09ca21 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -417,7 +417,7 @@ class MediaController(object): elif not hidden or controller.media_info.is_background or serviceItem.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/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/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index da706bdcb..e3860aeda 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -457,9 +457,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/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 827bfa156..2cd15a16f 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -35,16 +35,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): """ From f1aadde13cd30ecf89afb8d8bd07ad9f4e0b5210 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 6 Mar 2013 21:53:29 +0000 Subject: [PATCH 05/71] Initial Cherrypy implementation --- openlp/core/ui/exceptionform.py | 6 + openlp/core/ui/slidecontroller.py | 7 +- openlp/core/utils/__init__.py | 7 +- openlp/plugins/remotes/html/stage.js | 6 +- openlp/plugins/remotes/lib/httpauth.py | 192 ++++++++++++++++ openlp/plugins/remotes/lib/httpserver.py | 281 +++++++++++------------ openlp/plugins/remotes/lib/remotetab.py | 271 ++++++++++++++-------- openlp/plugins/remotes/remoteplugin.py | 5 + scripts/check_dependencies.py | 1 + 9 files changed, 534 insertions(+), 242 deletions(-) create mode 100644 openlp/plugins/remotes/lib/httpauth.py diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 50885b15b..c10f98d9b 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -69,6 +69,11 @@ try: MAKO_VERSION = mako.__version__ except ImportError: MAKO_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') @@ -138,6 +143,7 @@ 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'pyUNO bridge: %s\n' % UNO_VERSION if platform.system() == u'Linux': if os.environ.get(u'KDE_FULL_SESSION') == u'true': diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 84ea296d2..3a2c0b582 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -362,8 +362,9 @@ class SlideController(DisplayController): # Signals QtCore.QObject.connect(self.previewListWidget, QtCore.SIGNAL(u'clicked(QModelIndex)'), self.onSlideSelected) if self.isLive: + # 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.setWidgetVisible(self.loopList, False) self.toolbar.setWidgetVisible(self.wideMenu, False) else: @@ -867,9 +868,9 @@ class SlideController(DisplayController): """ Go to the requested slide """ - index = int(message[0]) - if not self.serviceItem: + if not self.serviceItem or not message[0]: return + index = int(message[0]) if self.serviceItem.is_command(): Registry().execute(u'%s_slide' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive, index]) self.updatePreview() diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 104567039..dd611d303 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -90,6 +90,7 @@ class AppLocation(object): VersionDir = 5 CacheDir = 6 LanguageDir = 7 + SharedData = 8 # Base path where data/config/cache dir is located BaseDir = None @@ -150,18 +151,18 @@ def _get_os_dir_path(dir_type): if sys.platform == u'win32': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp', u'data') - elif dir_type == AppLocation.LanguageDir: + elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp') elif sys.platform == u'darwin': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp', u'Data') - elif dir_type == AppLocation.LanguageDir: + elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp') else: - if dir_type == AppLocation.LanguageDir: + if dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: prefixes = [u'/usr/local', u'/usr'] for prefix in prefixes: directory = os.path.join(prefix, u'share', u'openlp') 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/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py new file mode 100644 index 000000000..ce3ea091e --- /dev/null +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +""" +The :mod:`http` module manages the HTTP authorisation logic. This code originates from +http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions + +""" + +import cherrypy +import urlparse + +SESSION_KEY = '_cp_openlp' + + +def check_credentials(user_name, password): + """ + Verifies credentials for username and password. + Returns None on success or a string describing the error on failure + """ + # @todo make from config + if user_name == 'openlp' and password == 'openlp': + return None + else: + return u"Incorrect username or password." + # if u.password != md5.new(password).hexdigest(): + # return u"Incorrect password" + + +def check_auth(*args, **kwargs): + """ + A tool that looks in config for 'auth.require'. If found and it + is not None, a login is required and the entry is evaluated as a list of + conditions that the user must fulfill + """ + print "check" + conditions = cherrypy.request.config.get('auth.require', None) + print conditions + print args, kwargs + print urlparse.urlparse(cherrypy.url()) + url = urlparse.urlparse(cherrypy.url()) + print urlparse.parse_qs(url.query) + if conditions is not None: + username = cherrypy.session.get(SESSION_KEY) + if username: + cherrypy.request.login = username + for condition in conditions: + # A condition is just a callable that returns true or false + if not condition(): + raise cherrypy.HTTPRedirect("/auth/login") + else: + raise cherrypy.HTTPRedirect("/auth/login") + +cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) + + +def require_auth(*conditions): + """ + A decorator that appends conditions to the auth.require config variable. + """ + print conditions + def decorate(f): + if not hasattr(f, '_cp_config'): + f._cp_config = dict() + if 'auth.require' not in f._cp_config: + f._cp_config['auth.require'] = [] + f._cp_config['auth.require'].extend(conditions) + print "a ", [f] + return f + return decorate + + +# Conditions are callables that return True +# if the user fulfills the conditions they define, False otherwise +# +# They can access the current username as cherrypy.request.login +# +# Define those at will however suits the application. + +#def member_of(groupname): +# def check(): +# # replace with actual check if is in +# return cherrypy.request.login == 'joe' and groupname == 'admin' +# return check + + +#def name_is(reqd_username): +# return lambda: reqd_username == cherrypy.request.login + +#def any_of(*conditions): +# """ +# Returns True if any of the conditions match +# """ +# def check(): +# for c in conditions: +# if c(): +# return True +# return False +# return check + +# By default all conditions are required, but this might still be +# needed if you want to use it inside of an any_of(...) condition +#def all_of(*conditions): +# """ +# Returns True if all of the conditions match +# """ +# def check(): +# for c in conditions: +# if not c(): +# return False +# return True +# return check +# Controller to provide login and logout actions + + +class AuthController(object): + + def on_login(self, username): + """ + Called on successful login + """ + + def on_logout(self, username): + """ + Called on logout + """ + + def get_loginform(self, username, msg="Enter login information", from_page="/"): + """ + Provides a login form + """ + return """ +
+ + %(msg)s
+ Username:
+ Password:
+ + """ % locals() + + @cherrypy.expose + def login(self, username=None, password=None, from_page="/"): + """ + Provides the actual login control + """ + if username is None or password is None: + return self.get_loginform("", from_page=from_page) + + error_msg = check_credentials(username, password) + if error_msg: + return self.get_loginform(username, error_msg, from_page) + else: + cherrypy.session[SESSION_KEY] = cherrypy.request.login = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or "/") + + @cherrypy.expose + def logout(self, from_page="/"): + sess = cherrypy.session + username = sess.get(SESSION_KEY, None) + sess[SESSION_KEY] = None + if username: + cherrypy.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page or "/") + diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 3b2c7439a..f4dd633e8 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -119,40 +119,23 @@ 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 openlp.plugins.remotes.lib.httpauth import AuthController, require_auth log = logging.getLogger(__name__) -class HttpResponse(object): - """ - A simple object to encapsulate a pseudo-http response. - """ - code = '200 OK' - content = '' - headers = { - 'Content-Type': 'text/html; charset="utf-8"\r\n' - } - - 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 - class HttpServer(object): """ Ability to control OpenLP via a web browser. """ + def __init__(self, plugin): """ Initialise the httpserver, and start the server. @@ -163,22 +146,28 @@ class HttpServer(object): self.connections = [] self.current_item = None self.current_slide = None - self.start_tcp() + self.conf = {'/files': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.html_dir}} + self.start_server() - 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. """ - log.debug(u'Start TCP server') + log.debug(u'Start CherryPy server') port = Settings().value(self.plugin.settingsSection + u'/port') address = Settings().value(self.plugin.settingsSection + u'/ip address') - self.server = QtNetwork.QTcpServer() - self.server.listen(QtNetwork.QHostAddress(address), port) + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port} + cherrypy.config.update(server_config) + cherrypy.config.update({'environment': 'embedded'}) + cherrypy.config.update({'engine.autoreload_on': False}) + cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) + cherrypy.engine.start() Registry().register_function(u'slidecontroller_live_changed', self.slide_change) Registry().register_function(u'slidecontroller_live_started', self.item_change) - QtCore.QObject.connect(self.server, QtCore.SIGNAL(u'newConnection()'), self.new_connection) log.debug(u'TCP listening on port %d' % port) def slide_change(self, row): @@ -193,50 +182,41 @@ class HttpServer(object): """ self.current_item = items[0] - def new_connection(self): - """ - A new http connection has been made. Create a client object to handle - communication. - """ - log.debug(u'new http connection') - socket = self.server.nextPendingConnection() - if socket: - self.connections.append(HttpConnection(self, socket)) - - def close_connection(self, connection): - """ - The connection has been closed. Clean up - """ - log.debug(u'close http connection') - if connection in self.connections: - self.connections.remove(connection) - def close(self): """ Close down the http server. """ log.debug(u'close http server') - self.server.close() + cherrypy.engine.exit() + cherrypy.engine.stop() class HttpConnection(object): """ - A single connection, this handles communication between the server - and the client. + A single connection, this handles communication between the server and the client. """ - def __init__(self, parent, socket): + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + + auth = AuthController() + + def __init__(self, parent): """ Initialise the http connection. Listen out for socket signals. """ - log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) - self.socket = socket + #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/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), @@ -245,17 +225,79 @@ class HttpConnection(object): (r'^/api/(.*)/live$', self.go_live), (r'^/api/(.*)/add$', self.add_to_service) ] - QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), self.ready_read) - QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'), self.disconnected) self.translate() + @cherrypy.expose + #@require_auth(auth) + def default(self, *args, **kwargs): + """ + Handles the requests for the main url. This is secure depending on settings. + """ + # Loop through the routes we set up earlier and execute them + return self._process_http_request(args, kwargs) + + @cherrypy.expose + def stage(self, *args, **kwargs): + """ + Handles the requests for the stage url. This is not secure. + """ + print "Stage" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + #return self.serve_file(u'stage') + return self._process_http_request(args, kwargs) + + @cherrypy.expose + def files(self, *args, **kwargs): + """ + Handles the requests for the stage url. This is not secure. + """ + print "files" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + print args + #return self.serve_file(args) + return self._process_http_request(args, kwargs) + + def _process_http_request(self, args, kwargs): + """ + Common function to process HTTP requests where secure or insecure + """ + print "common handler" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url + print [self.url_params] + response = None + for route, func in self.routes: + match = re.match(route, url.path) + if match: + print 'Route "%s" matched "%s"', route, url.path + 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.parent.current_item: current_unique_identifier = self.parent.current_item.unique_identifier else: current_unique_identifier = None - for item in self.service_manager.serviceItems: + for item in self.service_manager.service_items: service_item = item[u'service_item'] service_items.append({ u'id': unicode(service_item.unique_identifier), @@ -296,40 +338,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 @@ -339,6 +347,7 @@ class HttpConnection(object): Ultimately for i18n, this could first look for xx/file.html before falling back to file.html... where xx is the language, e.g. 'en' """ + print "serve_file", filename log.debug(u'serve file request %s' % filename) if not filename: filename = u'index.html' @@ -346,7 +355,7 @@ class HttpConnection(object): 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') + return self._http_not_found() ext = os.path.splitext(filename)[1] html = None if ext == u'.html': @@ -375,11 +384,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): """ @@ -389,24 +399,25 @@ class HttpConnection(object): u'service': self.service_manager.service_id, u'slide': self.parent.current_slide or 0, u'item': self.parent.current_item.unique_identifier if self.parent.current_item else u'', - u'twelve':Settings().value(u'remotes/twelve hour'), + u'twelve': Settings().value(u'remotes/twelve hour'), u'blank': self.live_controller.blankScreen.isChecked(), u'theme': self.live_controller.themeScreen.isChecked(), u'display': self.live_controller.desktopScreen.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): """ @@ -417,14 +428,14 @@ class HttpConnection(object): try: text = json.loads(self.url_params[u'data'][0])[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]) 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, type, action): """ @@ -465,34 +476,37 @@ class HttpConnection(object): try: data = json.loads(self.url_params[u'data'][0]) 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]) else: - Registry().execute(event) + Registry().execute(event, [0]) 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): + """ + List details of the Service and update the UI + """ 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'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': self._get_service_items()}}) else: event += u'_item' if self.url_params and self.url_params.get(u'data'): try: data = json.loads(self.url_params[u'data'][0]) except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() Registry().execute(event, data[u'request'][u'id']) 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): """ @@ -507,9 +521,8 @@ class HttpConnection(object): for plugin in self.plugin_manager.plugins: if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: 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'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': searches}}) def search(self, type): """ @@ -521,15 +534,15 @@ class HttpConnection(object): try: text = json.loads(self.url_params[u'data'][0])[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(type) if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: results = plugin.mediaItem.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, type): """ @@ -538,11 +551,11 @@ class HttpConnection(object): try: id = json.loads(self.url_params[u'data'][0])[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(type) if plugin.status == PluginStatus.Active and plugin.mediaItem: plugin.mediaItem.goLive(id, remote=True) - return HttpResponse(code=u'200 OK') + return self._http_success() def add_to_service(self, type): """ @@ -551,38 +564,22 @@ class HttpConnection(object): try: id = json.loads(self.url_params[u'data'][0])[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(type) if plugin.status == PluginStatus.Active and plugin.mediaItem: item_id = plugin.mediaItem.createItemFromId(id) plugin.mediaItem.addToService(item_id, remote=True) - return HttpResponse(code=u'200 OK') + 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): + cherrypy.response.status = 200 - def disconnected(self): - """ - The client has disconnected. Tidy up - """ - log.debug(u'socket disconnected') - self.close() + def _http_bad_request(self): + cherrypy.response.status = 400 - def close(self): - """ - The server has closed the connection. Tidy up - """ - if not self.socket: - return - log.debug(u'close socket') - self.socket.close() - self.socket = None - self.parent.close_connection(self) + def _http_not_found(self): + cherrypy.response.status = 404 + cherrypy.response.body = ["Sorry, an error occured"] def _get_service_manager(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 38b8753ab..7d23f500f 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.utils import AppLocation ZERO_URL = u'0.0.0.0' @@ -45,117 +48,203 @@ class RemoteTab(SettingsTab): def setupUi(self): self.setObjectName(u'RemoteTab') SettingsTab.setupUi(self) - self.serverSettingsGroupBox = QtGui.QGroupBox(self.leftColumn) - self.serverSettingsGroupBox.setObjectName(u'serverSettingsGroupBox') - self.serverSettingsLayout = QtGui.QFormLayout(self.serverSettingsGroupBox) - self.serverSettingsLayout.setObjectName(u'serverSettingsLayout') - self.addressLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.addressLabel.setObjectName(u'addressLabel') - self.addressEdit = QtGui.QLineEdit(self.serverSettingsGroupBox) - self.addressEdit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) - self.addressEdit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp( - u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self)) - self.addressEdit.setObjectName(u'addressEdit') - QtCore.QObject.connect(self.addressEdit, QtCore.SIGNAL(u'textChanged(const QString&)'), self.setUrls) - self.serverSettingsLayout.addRow(self.addressLabel, self.addressEdit) - self.twelveHourCheckBox = QtGui.QCheckBox(self.serverSettingsGroupBox) - self.twelveHourCheckBox.setObjectName(u'twelveHourCheckBox') - self.serverSettingsLayout.addRow(self.twelveHourCheckBox) - self.portLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.portLabel.setObjectName(u'portLabel') - self.portSpinBox = QtGui.QSpinBox(self.serverSettingsGroupBox) - self.portSpinBox.setMaximum(32767) - self.portSpinBox.setObjectName(u'portSpinBox') - QtCore.QObject.connect(self.portSpinBox, QtCore.SIGNAL(u'valueChanged(int)'), self.setUrls) - self.serverSettingsLayout.addRow(self.portLabel, self.portSpinBox) - self.remoteUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.remoteUrlLabel.setObjectName(u'remoteUrlLabel') - self.remoteUrl = QtGui.QLabel(self.serverSettingsGroupBox) - self.remoteUrl.setObjectName(u'remoteUrl') - self.remoteUrl.setOpenExternalLinks(True) - self.serverSettingsLayout.addRow(self.remoteUrlLabel, self.remoteUrl) - self.stageUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox) - self.stageUrlLabel.setObjectName(u'stageUrlLabel') - self.stageUrl = QtGui.QLabel(self.serverSettingsGroupBox) - self.stageUrl.setObjectName(u'stageUrl') - self.stageUrl.setOpenExternalLinks(True) - self.serverSettingsLayout.addRow(self.stageUrlLabel, self.stageUrl) - self.leftLayout.addWidget(self.serverSettingsGroupBox) - self.androidAppGroupBox = QtGui.QGroupBox(self.rightColumn) - self.androidAppGroupBox.setObjectName(u'androidAppGroupBox') - self.rightLayout.addWidget(self.androidAppGroupBox) - self.qrLayout = QtGui.QVBoxLayout(self.androidAppGroupBox) - self.qrLayout.setObjectName(u'qrLayout') - self.qrCodeLabel = QtGui.QLabel(self.androidAppGroupBox) - self.qrCodeLabel.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png')) - self.qrCodeLabel.setAlignment(QtCore.Qt.AlignCenter) - self.qrCodeLabel.setObjectName(u'qrCodeLabel') - self.qrLayout.addWidget(self.qrCodeLabel) - self.qrDescriptionLabel = QtGui.QLabel(self.androidAppGroupBox) - self.qrDescriptionLabel.setObjectName(u'qrDescriptionLabel') - self.qrDescriptionLabel.setOpenExternalLinks(True) - self.qrDescriptionLabel.setWordWrap(True) - self.qrLayout.addWidget(self.qrDescriptionLabel) + self.server_settings_group_box = QtGui.QGroupBox(self.leftColumn) + self.server_settings_group_box.setObjectName(u'server_settings_group_box') + self.server_settings_layout = QtGui.QFormLayout(self.server_settings_group_box) + self.server_settings_layout.setObjectName(u'server_settings_layout') + self.address_label = QtGui.QLabel(self.server_settings_group_box) + 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.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.leftLayout.addWidget(self.server_settings_group_box) + self.http_settings_group_box = QtGui.QGroupBox(self.leftColumn) + 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.http_settings_group_box) + self.port_spin_box.setMaximum(32767) + self.port_spin_box.setObjectName(u'port_spin_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.http_settings_group_box) + self.remote_url.setObjectName(u'remote_url') + self.remote_url.setOpenExternalLinks(True) + 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.http_settings_group_box) + self.stage_url.setObjectName(u'stage_url') + self.stage_url.setOpenExternalLinks(True) + self.http_setting_layout.addRow(self.stage_url_label, self.stage_url) + self.leftLayout.addWidget(self.http_settings_group_box) + self.https_settings_group_box = QtGui.QGroupBox(self.leftColumn) + 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.leftLayout.addWidget(self.https_settings_group_box) + self.user_login_group_box = QtGui.QGroupBox(self.leftColumn) + 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.leftLayout.addWidget(self.user_login_group_box) + self.android_app_group_box = QtGui.QGroupBox(self.rightColumn) + self.android_app_group_box.setObjectName(u'android_app_group_box') + self.rightLayout.addWidget(self.android_app_group_box) + self.qr_layout = QtGui.QVBoxLayout(self.android_app_group_box) + self.qr_layout.setObjectName(u'qr_layout') + self.qr_code_label = QtGui.QLabel(self.android_app_group_box) + self.qr_code_label.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png')) + self.qr_code_label.setAlignment(QtCore.Qt.AlignCenter) + self.qr_code_label.setObjectName(u'qr_code_label') + self.qr_layout.addWidget(self.qr_code_label) + self.qr_description_label = QtGui.QLabel(self.android_app_group_box) + self.qr_description_label.setObjectName(u'qr_description_label') + self.qr_description_label.setOpenExternalLinks(True) + self.qr_description_label.setWordWrap(True) + self.qr_layout.addWidget(self.qr_description_label) self.leftLayout.addStretch() self.rightLayout.addStretch() - QtCore.QObject.connect(self.twelveHourCheckBox, QtCore.SIGNAL(u'stateChanged(int)'), - 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) def retranslateUi(self): - self.serverSettingsGroupBox.setTitle( - translate('RemotePlugin.RemoteTab', 'Server Settings')) - self.addressLabel.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:')) - self.portLabel.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) - self.remoteUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) - self.stageUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) - self.twelveHourCheckBox.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) - self.androidAppGroupBox.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) - self.qrDescriptionLabel.setText(translate('RemotePlugin.RemoteTab', - 'Scan the QR code or click download to install the ' - 'Android app from Google Play.')) + self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) + self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:')) + self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) + self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) + self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) + self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) + self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) + self.qr_description_label.setText(translate('RemotePlugin.RemoteTab', + '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 setUrls(self): + def set_urls(self): ipAddress = u'localhost' - if self.addressEdit.text() == ZERO_URL: - ifaces = QtNetwork.QNetworkInterface.allInterfaces() - for iface in ifaces: - if not iface.isValid(): + if self.address_edit.text() == ZERO_URL: + interfaces = QtNetwork.QNetworkInterface.allInterfaces() + for interface in interfaces: + if not interface.isValid(): continue - if not (iface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)): + if not (interface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)): continue - for addr in iface.addressEntries(): - ip = addr.ip() + for address in interface.addressEntries(): + ip = address.ip() if ip.protocol() == 0 and ip != QtNetwork.QHostAddress.LocalHost: ipAddress = ip break else: - ipAddress = self.addressEdit.text() - url = u'http://%s:%s/' % (ipAddress, self.portSpinBox.value()) - self.remoteUrl.setText(u'%s' % (url, url)) - url += u'stage' - self.stageUrl.setText(u'%s' % (url, url)) + ipAddress = self.address_edit.text() + http_url = u'http://%s:%s/' % (ipAddress, self.port_spin_box.value()) + https_url = u'https://%s:%s/' % (ipAddress, 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): - self.portSpinBox.setValue(Settings().value(self.settingsSection + u'/port')) - self.addressEdit.setText(Settings().value(self.settingsSection + u'/ip address')) - self.twelveHour = Settings().value(self.settingsSection + u'/twelve hour') - self.twelveHourCheckBox.setChecked(self.twelveHour) - self.setUrls() + self.port_spin_box.setValue(Settings().value(self.settingsSection + u'/port')) + self.https_port_spin_box.setValue(Settings().value(self.settingsSection + u'/https port')) + self.address_edit.setText(Settings().value(self.settingsSection + u'/ip address')) + self.twelve_hour = Settings().value(self.settingsSection + u'/twelve hour') + self.twelve_hour_check_box.setChecked(self.twelve_hour) + shared_data = AppLocation.get_directory(AppLocation.SharedData) + if not os.path.exists(os.path.join(shared_data, u'openlp.crt')) or \ + not os.path.exists(os.path.join(shared_data, 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.settingsSection + 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.settingsSection + u'/authentication enabled')) + self.user_id.setText(Settings().value(self.settingsSection + u'/user id')) + self.password.setText(Settings().value(self.settingsSection + u'/password')) + self.set_urls() def save(self): changed = False - if Settings().value(self.settingsSection + u'/ip address') != self.addressEdit.text() or \ - Settings().value(self.settingsSection + u'/port') != self.portSpinBox.value(): + if Settings().value(self.settingsSection + u'/ip address') != self.address_edit.text() or \ + Settings().value(self.settingsSection + u'/port') != self.port_spin_box.value() or \ + Settings().value(self.settingsSection + u'/https port') != self.https_port_spin_box.value() or \ + Settings().value(self.settingsSection + u'/https enabled') != self.https_settings_group_box.isChecked(): changed = True - Settings().setValue(self.settingsSection + u'/port', self.portSpinBox.value()) - Settings().setValue(self.settingsSection + u'/ip address', self.addressEdit.text()) - Settings().setValue(self.settingsSection + u'/twelve hour', self.twelveHour) + Settings().setValue(self.settingsSection + u'/port', self.port_spin_box.value()) + Settings().setValue(self.settingsSection + u'/https port', self.https_port_spin_box.value()) + Settings().setValue(self.settingsSection + u'/https enabled', self.https_settings_group_box.isChecked()) + Settings().setValue(self.settingsSection + u'/ip address', self.address_edit.text()) + Settings().setValue(self.settingsSection + u'/twelve hour', self.twelve_hour) + Settings().setValue(self.settingsSection + u'/authentication enabled', self.user_login_group_box.isChecked()) + Settings().setValue(self.settingsSection + u'/user id', self.user_id.text()) + Settings().setValue(self.settingsSection + u'/password', self.password.text()) if changed: Registry().register_function(u'remotes_config_updated') - def onTwelveHourCheckBoxChanged(self, check_state): - self.twelveHour = False + def on_twelve_hour_check_box_changed(self, check_state): + self.twelve_hour = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: - self.twelveHour = True + self.twelve_hour = True diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index e028dfcbb..7c1541ea6 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -37,6 +37,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' } diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 3485b8505..2ff62cf65 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -81,6 +81,7 @@ MODULES = [ 'enchant', 'BeautifulSoup', 'mako', + 'cherrypy', 'migrate', 'uno', ] From c62dab074ecaa31790089d809eb76a4530de53b3 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 8 Mar 2013 19:15:49 +0000 Subject: [PATCH 06/71] Basic Authentication now working --- openlp/core/ui/slidecontroller.py | 2 +- openlp/plugins/remotes/lib/httpauth.py | 18 ++++++++++------ openlp/plugins/remotes/lib/httpserver.py | 27 +++++++++++------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 98868bbcb..5fdd58aae 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -860,7 +860,7 @@ class SlideController(DisplayController): """ Go to the requested slide """ - if not self.serviceItem or not message[0]: + if not self.service_item or not message[0]: return index = int(message[0]) if not self.service_item: diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index ce3ea091e..6ed42cebd 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -45,6 +45,7 @@ def check_credentials(user_name, password): Returns None on success or a string describing the error on failure """ # @todo make from config + print "check_credentials" if user_name == 'openlp' and password == 'openlp': return None else: @@ -59,18 +60,17 @@ def check_auth(*args, **kwargs): is not None, a login is required and the entry is evaluated as a list of conditions that the user must fulfill """ - print "check" + print "check_auth" conditions = cherrypy.request.config.get('auth.require', None) + print urlparse.urlparse(cherrypy.url()), conditions print conditions - print args, kwargs - print urlparse.urlparse(cherrypy.url()) - url = urlparse.urlparse(cherrypy.url()) - print urlparse.parse_qs(url.query) if conditions is not None: username = cherrypy.session.get(SESSION_KEY) + print username if username: cherrypy.request.login = username for condition in conditions: + print "c ", condition # A condition is just a callable that returns true or false if not condition(): raise cherrypy.HTTPRedirect("/auth/login") @@ -84,14 +84,15 @@ def require_auth(*conditions): """ A decorator that appends conditions to the auth.require config variable. """ - print conditions def decorate(f): + """ + Lets process a decoration. + """ if not hasattr(f, '_cp_config'): f._cp_config = dict() if 'auth.require' not in f._cp_config: f._cp_config['auth.require'] = [] f._cp_config['auth.require'].extend(conditions) - print "a ", [f] return f return decorate @@ -182,6 +183,9 @@ class AuthController(object): @cherrypy.expose def logout(self, from_page="/"): + """ + Provides the actual logout functions + """ sess = cherrypy.session username = sess.get(SESSION_KEY, None) sess[SESSION_KEY] = None diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index dc9422932..345c0b1b7 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -205,8 +205,6 @@ class HttpConnection(object): """ Initialise the http connection. Listen out for socket signals. """ - #log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) - #self.socket = socket self.parent = parent self.routes = [ (u'^/$', self.serve_file), @@ -215,8 +213,9 @@ class HttpConnection(object): (r'^/api/poll$', self.poll), (r'^/stage/api/poll$', self.poll), (r'^/api/controller/(live|preview)/(.*)$', self.controller), - (r'^/stage/api/controller/live/(.*)$', 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), @@ -227,11 +226,15 @@ class HttpConnection(object): self.translate() @cherrypy.expose - #@require_auth(auth) + @require_auth() def default(self, *args, **kwargs): """ - Handles the requests for the main url. This is secure depending on settings. + Handles the requests for the main url. This is secure depending on settings in config. """ + print "default" + url = urlparse.urlparse(cherrypy.url()) + self.url_params = urlparse.parse_qs(url.query) + print url # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -244,22 +247,17 @@ class HttpConnection(object): url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) print url - print [self.url_params] - #return self.serve_file(u'stage') return self._process_http_request(args, kwargs) @cherrypy.expose def files(self, *args, **kwargs): """ - Handles the requests for the stage url. This is not secure. + Handles the requests for the files url. This is not secure. """ print "files" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) print url - print [self.url_params] - print args - #return self.serve_file(args) return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): @@ -269,13 +267,11 @@ class HttpConnection(object): print "common handler" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url - print [self.url_params] response = None for route, func in self.routes: match = re.match(route, url.path) if match: - print 'Route "%s" matched "%s"', route, url.path + print 'Route "%s" matched "%s"', route, url.path, func log.debug('Route "%s" matched "%s"', route, url.path) args = [] for param in match.groups(): @@ -346,7 +342,6 @@ class HttpConnection(object): Ultimately for i18n, this could first look for xx/file.html before falling back to file.html... where xx is the language, e.g. 'en' """ - print "serve_file", filename log.debug(u'serve file request %s' % filename) if not filename: filename = u'index.html' @@ -483,6 +478,7 @@ class HttpConnection(object): Registry().execute(event, [0]) json_data = {u'results': {u'success': True}} cherrypy.response.headers['Content-Type'] = u'application/json' + print json.dumps(json_data) return json.dumps(json_data) def service(self, action): @@ -549,6 +545,7 @@ class HttpConnection(object): """ Go live on an item of type ``plugin``. """ + print "go_live" try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: From 5c79832bcc8f1448292c7ec5c1bfe5775ea75fd9 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 11 Mar 2013 21:00:00 +0000 Subject: [PATCH 07/71] More updates to authentication --- openlp/core/lib/settingstab.py | 1 + openlp/core/ui/slidecontroller.py | 2 - openlp/plugins/remotes/lib/httpauth.py | 90 ++++++++---------------- openlp/plugins/remotes/lib/httpserver.py | 32 ++++----- openlp/plugins/remotes/lib/remotetab.py | 7 +- 5 files changed, 46 insertions(+), 86 deletions(-) diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index 5b8a01fc6..51abfbe03 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -36,6 +36,7 @@ from PyQt4 import QtGui from openlp.core.lib import Registry + class SettingsTab(QtGui.QWidget): """ SettingsTab is a helper widget for plugins to define Tabs for the settings diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 5fdd58aae..44ae023b6 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -860,8 +860,6 @@ class SlideController(DisplayController): """ Go to the requested slide """ - if not self.service_item or not message[0]: - return index = int(message[0]) if not self.service_item: return diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 6ed42cebd..7e0e2ebe5 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -34,24 +34,24 @@ http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions """ import cherrypy -import urlparse +import logging + +from openlp.core.lib import Settings SESSION_KEY = '_cp_openlp' +log = logging.getLogger(__name__) + def check_credentials(user_name, password): """ Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ - # @todo make from config - print "check_credentials" - if user_name == 'openlp' and password == 'openlp': + if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: return u"Incorrect username or password." - # if u.password != md5.new(password).hexdigest(): - # return u"Incorrect password" def check_auth(*args, **kwargs): @@ -60,17 +60,14 @@ def check_auth(*args, **kwargs): is not None, a login is required and the entry is evaluated as a list of conditions that the user must fulfill """ - print "check_auth" conditions = cherrypy.request.config.get('auth.require', None) - print urlparse.urlparse(cherrypy.url()), conditions - print conditions + if not Settings().value(u'remotes/authentication enabled'): + return None if conditions is not None: username = cherrypy.session.get(SESSION_KEY) - print username if username: cherrypy.request.login = username for condition in conditions: - print "c ", condition # A condition is just a callable that returns true or false if not condition(): raise cherrypy.HTTPRedirect("/auth/login") @@ -97,49 +94,6 @@ def require_auth(*conditions): return decorate -# Conditions are callables that return True -# if the user fulfills the conditions they define, False otherwise -# -# They can access the current username as cherrypy.request.login -# -# Define those at will however suits the application. - -#def member_of(groupname): -# def check(): -# # replace with actual check if is in -# return cherrypy.request.login == 'joe' and groupname == 'admin' -# return check - - -#def name_is(reqd_username): -# return lambda: reqd_username == cherrypy.request.login - -#def any_of(*conditions): -# """ -# Returns True if any of the conditions match -# """ -# def check(): -# for c in conditions: -# if c(): -# return True -# return False -# return check - -# By default all conditions are required, but this might still be -# needed if you want to use it inside of an any_of(...) condition -#def all_of(*conditions): -# """ -# Returns True if all of the conditions match -# """ -# def check(): -# for c in conditions: -# if not c(): -# return False -# return True -# return check -# Controller to provide login and logout actions - - class AuthController(object): def on_login(self, username): @@ -156,14 +110,26 @@ class AuthController(object): """ Provides a login form """ - return """ - - - %(msg)s
- Username:
- Password:
- - """ % locals() + return """ + + + + User Login + + + + + + + + + + + %(msg)s
+ Username:
+ Password:
+ + """ % locals() @cherrypy.expose def login(self, username=None, password=None, from_page="/"): diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 345c0b1b7..211608858 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -130,6 +130,7 @@ from openlp.plugins.remotes.lib.httpauth import AuthController, require_auth log = logging.getLogger(__name__) + class HttpServer(object): """ Ability to control OpenLP via a web browser. @@ -156,10 +157,19 @@ class HttpServer(object): clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') - port = Settings().value(self.plugin.settingsSection + u'/port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port} + if Settings().value(self.plugin.settingsSection + u'/https enabled'): + port = Settings().value(self.plugin.settingsSection + u'/https port') + address = Settings().value(self.plugin.settingsSection + u'/ip address') + shared_data = AppLocation.get_directory(AppLocation.SharedData) + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port, + u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), + u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} + else: + port = Settings().value(self.plugin.settingsSection + u'/port') + address = Settings().value(self.plugin.settingsSection + u'/ip address') + server_config = {u'server.socket_host': str(address), + u'server.socket_port': port} cherrypy.config.update(server_config) cherrypy.config.update({'environment': 'embedded'}) cherrypy.config.update({'engine.autoreload_on': False}) @@ -231,10 +241,8 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - print "default" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -243,10 +251,8 @@ class HttpConnection(object): """ Handles the requests for the stage url. This is not secure. """ - print "Stage" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url return self._process_http_request(args, kwargs) @cherrypy.expose @@ -254,24 +260,20 @@ class HttpConnection(object): """ Handles the requests for the files url. This is not secure. """ - print "files" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) - print url return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): """ Common function to process HTTP requests where secure or insecure """ - print "common handler" url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) response = None for route, func in self.routes: match = re.match(route, url.path) if match: - print 'Route "%s" matched "%s"', route, url.path, func log.debug('Route "%s" matched "%s"', route, url.path) args = [] for param in match.groups(): @@ -474,11 +476,8 @@ class HttpConnection(object): # This slot expects an int within a list. id = data[u'request'][u'id'] Registry().execute(event, [id]) - else: - Registry().execute(event, [0]) json_data = {u'results': {u'success': True}} cherrypy.response.headers['Content-Type'] = u'application/json' - print json.dumps(json_data) return json.dumps(json_data) def service(self, action): @@ -545,7 +544,6 @@ class HttpConnection(object): """ Go live on an item of type ``plugin``. """ - print "go_live" try: id = json.loads(self.url_params[u'data'][0])[u'request'][u'id'] except KeyError, ValueError: @@ -577,7 +575,7 @@ class HttpConnection(object): def _http_not_found(self): cherrypy.response.status = 404 - cherrypy.response.body = ["Sorry, an error occured"] + cherrypy.response.body = ["Sorry, an error occurred "] def _get_service_manager(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index b5c911257..7d47988fc 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -56,15 +56,13 @@ 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.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.leftLayout.addWidget(self.server_settings_group_box) self.http_settings_group_box = QtGui.QGroupBox(self.leftColumn) self.http_settings_group_box.setObjectName(u'http_settings_group_box') @@ -75,7 +73,6 @@ class RemoteTab(SettingsTab): 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.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') @@ -244,7 +241,7 @@ class RemoteTab(SettingsTab): Settings().setValue(self.settingsSection + u'/user id', self.user_id.text()) Settings().setValue(self.settingsSection + u'/password', self.password.text()) if changed: - Registry().register_function(u'remotes_config_updated') + Registry().execute(u'remotes_config_updated') def on_twelve_hour_check_box_changed(self, check_state): From c580aac61be53f0de54667d1daa06b9ebacabd55 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 13 Mar 2013 19:09:26 +0000 Subject: [PATCH 08/71] custom login page --- openlp/plugins/remotes/html/login.html | 51 ++++++++++++++++++++++++ openlp/plugins/remotes/html/openlp.css | 8 ++++ openlp/plugins/remotes/lib/httpauth.py | 51 +++++++++++++----------- openlp/plugins/remotes/lib/httpserver.py | 2 + 4 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 openlp/plugins/remotes/html/login.html diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html new file mode 100644 index 000000000..4441958de --- /dev/null +++ b/openlp/plugins/remotes/html/login.html @@ -0,0 +1,51 @@ + + + + + + + + ${title} + + + + + + + + + + +

${message}

+

+ User name:
+ Password:
+ + + \ No newline at end of file diff --git a/openlp/plugins/remotes/html/openlp.css b/openlp/plugins/remotes/html/openlp.css index 4bc1bf907..60a8fe625 100644 --- a/openlp/plugins/remotes/html/openlp.css +++ b/openlp/plugins/remotes/html/openlp.css @@ -36,3 +36,11 @@ .ui-li .ui-btn-text a.ui-link-inherit{ white-space: normal; } + +.ui-page{ + padding: 100px 100px 100px 100px; + width: 300px; +} +.ui-input-text{ + width: 30px; +} \ No newline at end of file diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 7e0e2ebe5..d46620855 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -35,8 +35,12 @@ http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions import cherrypy import logging +import os + +from mako.template import Template from openlp.core.lib import Settings +from openlp.core.utils import AppLocation, translate SESSION_KEY = '_cp_openlp' @@ -48,6 +52,7 @@ def check_credentials(user_name, password): Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ + print "check" if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: @@ -70,9 +75,12 @@ def check_auth(*args, **kwargs): for condition in conditions: # A condition is just a callable that returns true or false if not condition(): + print "r1" raise cherrypy.HTTPRedirect("/auth/login") else: + print "r2" raise cherrypy.HTTPRedirect("/auth/login") + print "r3" cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) @@ -100,36 +108,31 @@ class AuthController(object): """ Called on successful login """ + pass def on_logout(self, username): """ Called on logout """ + pass - def get_loginform(self, username, msg="Enter login information", from_page="/"): + def get_login_form(self, username, message=None, from_page="/"): """ Provides a login form """ - return """ - - - - User Login - - - - - - - - -
- - %(msg)s
- Username:
- Password:
- - """ % locals() + if not message: + message = translate('RemotePlugin.Mobile', 'Enter login information') + variables = { + 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), + 'from_page': from_page, + 'message': message, + 'username': username + } + directory = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') + login_html = os.path.normpath(os.path.join(directory, u'login.html')) + html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) + cherrypy.response.headers['Content-Type'] = u'text/html' + return html @cherrypy.expose def login(self, username=None, password=None, from_page="/"): @@ -137,14 +140,14 @@ class AuthController(object): Provides the actual login control """ if username is None or password is None: - return self.get_loginform("", from_page=from_page) - + return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) if error_msg: - return self.get_loginform(username, error_msg, from_page) + return self.get_login_form(username, from_page, error_msg,) else: cherrypy.session[SESSION_KEY] = cherrypy.request.login = username self.on_login(username) + print from_page raise cherrypy.HTTPRedirect(from_page or "/") @cherrypy.expose diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 211608858..db6143eb0 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -174,6 +174,8 @@ class HttpServer(object): cherrypy.config.update({'environment': 'embedded'}) cherrypy.config.update({'engine.autoreload_on': False}) cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) + # Turn off the flood of access messages cause by poll + cherrypy.log.access_log.propagate = False cherrypy.engine.start() Registry().register_function(u'slidecontroller_live_changed', self.slide_change) Registry().register_function(u'slidecontroller_live_started', self.item_change) From 3c32bc75011eac3b7ed3d07d90c8f818a6dab80d Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 13 Mar 2013 19:51:56 +0000 Subject: [PATCH 09/71] Added tests --- openlp/plugins/remotes/lib/httpauth.py | 2 +- .../openlp_core_lib/test_settings.py | 4 +- .../openlp_plugins/remotes/__init__.py | 1 + .../openlp_plugins/remotes/test_auth.py | 65 +++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/functional/openlp_plugins/remotes/__init__.py create mode 100644 tests/functional/openlp_plugins/remotes/test_auth.py diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index d46620855..6fe4197e2 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -56,7 +56,7 @@ def check_credentials(user_name, password): if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: - return u"Incorrect username or password." + return translate('RemotePlugin.Mobile', 'Incorrect username or password.') def check_auth(*args, **kwargs): diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 827bfa156..b06bb4eac 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 diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..f87606f07 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/__init__.py @@ -0,0 +1 @@ +__author__ = 'tim' diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py new file mode 100644 index 000000000..a300c0127 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -0,0 +1,65 @@ +""" +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.httpauth import check_credentials +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 TestLib(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__) + + 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) + os.unlink(Settings().fileName()) + + def check_credentials_test(self): + """ + Test the clean_string() function + """ + # GIVEN: A user and password + Settings().setValue(u'remotes/user id', u'twinkle') + Settings().setValue(u'remotes/password', u'mongoose') + + # WHEN: We run the string through the function + authenticated = check_credentials(u'', u'') + + # THEN: The string should be cleaned up and lower-cased + self.assertEqual(authenticated, u'Incorrect username or password.', + u'The return should be a error message string') + + # WHEN: We run the string through the function + authenticated = check_credentials(u'twinkle', u'mongoose') + + # THEN: The string should be cleaned up and lower-cased + self.assertEqual(authenticated, None, u'The return should be a None string') From d04dbd791f9ef141f947ca555fbc014cf84fd614 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 15 Mar 2013 08:40:00 +0000 Subject: [PATCH 10/71] more changes --- openlp/plugins/remotes/html/login.html | 2 +- openlp/plugins/remotes/html/openlp.css | 8 -------- openlp/plugins/remotes/html/openlp.js | 2 +- openlp/plugins/remotes/lib/httpauth.py | 3 --- openlp/plugins/remotes/lib/httpserver.py | 7 +------ 5 files changed, 3 insertions(+), 19 deletions(-) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 4441958de..736d3f0ab 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -33,7 +33,7 @@ ${title} - + diff --git a/openlp/plugins/remotes/html/openlp.css b/openlp/plugins/remotes/html/openlp.css index 60a8fe625..4bc1bf907 100644 --- a/openlp/plugins/remotes/html/openlp.css +++ b/openlp/plugins/remotes/html/openlp.css @@ -36,11 +36,3 @@ .ui-li .ui-btn-text a.ui-link-inherit{ white-space: normal; } - -.ui-page{ - padding: 100px 100px 100px 100px; - width: 300px; -} -.ui-input-text{ - width: 30px; -} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 00877e332..7c5c19e32 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -359,5 +359,5 @@ $.ajaxSetup({cache: false}); $("#search").live("pageinit", function (event) { OpenLP.getSearchablePlugins(); }); -setInterval("OpenLP.pollServer();", 500); +setInterval("OpenLP.pollServer();", 5000); OpenLP.pollServer(); diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index 6fe4197e2..bd3c1f911 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -75,12 +75,9 @@ def check_auth(*args, **kwargs): for condition in conditions: # A condition is just a callable that returns true or false if not condition(): - print "r1" raise cherrypy.HTTPRedirect("/auth/login") else: - print "r2" raise cherrypy.HTTPRedirect("/auth/login") - print "r3" cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index b8acc574f..308533b9d 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -144,8 +144,6 @@ class HttpServer(object): self.plugin = plugin self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') self.connections = [] - self.current_item = None - self.current_slide = None self.conf = {'/files': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.html_dir}} self.start_server() @@ -177,8 +175,6 @@ class HttpServer(object): # Turn off the flood of access messages cause by poll cherrypy.log.access_log.propagate = False cherrypy.engine.start() - Registry().register_function(u'slidecontroller_live_changed', self.slide_change) - Registry().register_function(u'slidecontroller_live_started', self.item_change) log.debug(u'TCP listening on port %d' % port) def close(self): @@ -481,8 +477,7 @@ class HttpConnection(object): if action == u'list': cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'items': self._get_service_items()}}) - else: - event += u'_item' + event += u'_item' if self.url_params and self.url_params.get(u'data'): try: data = json.loads(self.url_params[u'data'][0]) From 0f6216d653f1df071556e6e57867d8c2c477ff84 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 17 Mar 2013 21:20:40 +0000 Subject: [PATCH 11/71] More changes --- openlp/core/ui/servicemanager.py | 5 ++++- openlp/core/ui/slidecontroller.py | 2 +- openlp/plugins/remotes/html/openlp.js | 4 ++-- openlp/plugins/remotes/lib/httpserver.py | 17 +++++++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 9e99e2303..c7ab8dd77 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -270,7 +270,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): """ @@ -313,6 +312,9 @@ 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 + print self + QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item) def set_modified(self, modified=True): """ @@ -1008,6 +1010,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): """ Called by a signal to select a specific item. """ + print "hello", message self.set_item(int(message)) def set_item(self, index): diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 0c22a6353..3b2824fda 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -99,7 +99,7 @@ class SlideController(DisplayController): u'delay_spin_box' ] self.audio_list = [ - u'audio_pause_item', + u'audioPauseItem', u'audio_time_label' ] self.wide_menu = [ diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 7c5c19e32..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; @@ -359,5 +359,5 @@ $.ajaxSetup({cache: false}); $("#search").live("pageinit", function (event) { OpenLP.getSearchablePlugins(); }); -setInterval("OpenLP.pollServer();", 5000); +setInterval("OpenLP.pollServer();", 500); OpenLP.pollServer(); diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 308533b9d..a594c1bca 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -199,7 +199,7 @@ class HttpConnection(object): def __init__(self, parent): """ - Initialise the http connection. Listen out for socket signals. + Initialise the CherryPy Server """ self.parent = parent self.routes = [ @@ -229,6 +229,9 @@ class HttpConnection(object): """ url = urlparse.urlparse(cherrypy.url()) self.url_params = urlparse.parse_qs(url.query) + self.request_data = None + if isinstance(kwargs, dict): + self.request_data = kwargs.get(u'data', None) # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @@ -255,7 +258,7 @@ class HttpConnection(object): Common function to process HTTP requests where secure or insecure """ url = urlparse.urlparse(cherrypy.url()) - self.url_params = urlparse.parse_qs(url.query) + self.url_params = kwargs response = None for route, func in self.routes: match = re.match(route, url.path) @@ -478,13 +481,15 @@ class HttpConnection(object): cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'items': self._get_service_items()}}) event += u'_item' - 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]) - except KeyError, ValueError: + data = json.loads(self.request_data)[u'request'][u'id'] + except KeyError: return self._http_bad_request() - Registry().execute(event, data[u'request'][u'id']) + print "A", event , data + self.service_manager.emit(QtCore.SIGNAL(event, data)) else: + print "B", event Registry().execute(event) cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': {u'success': True}}) From 729c93b70b6a154dfc723405dd40c75410a57c8b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 20 Mar 2013 20:17:00 +0000 Subject: [PATCH 12/71] More changes --- openlp/core/ui/servicemanager.py | 2 +- openlp/plugins/remotes/lib/httpserver.py | 40 +++++++++++------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 97c6ca2db..7c3745299 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1008,7 +1008,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. """ print "hello", message self.set_item(int(message)) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index a594c1bca..368bf0192 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -155,17 +155,17 @@ class HttpServer(object): clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') - if Settings().value(self.plugin.settingsSection + u'/https enabled'): - port = Settings().value(self.plugin.settingsSection + u'/https port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') + if Settings().value(self.plugin.settings_section + u'/https enabled'): + port = Settings().value(self.plugin.settings_section + u'/https port') + address = Settings().value(self.plugin.settings_section + u'/ip address') shared_data = AppLocation.get_directory(AppLocation.SharedData) server_config = {u'server.socket_host': str(address), u'server.socket_port': port, u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} else: - port = Settings().value(self.plugin.settingsSection + u'/port') - address = Settings().value(self.plugin.settingsSection + u'/ip address') + port = Settings().value(self.plugin.settings_section + u'/port') + address = Settings().value(self.plugin.settings_section + u'/ip address') server_config = {u'server.socket_host': str(address), u'server.socket_port': port} cherrypy.config.update(server_config) @@ -214,7 +214,7 @@ class HttpConnection(object): (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) @@ -456,9 +456,9 @@ 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 self._http_bad_request() log.info(data) @@ -486,15 +486,13 @@ class HttpConnection(object): data = json.loads(self.request_data)[u'request'][u'id'] except KeyError: return self._http_bad_request() - print "A", event , data - self.service_manager.emit(QtCore.SIGNAL(event, data)) + self.service_manager.emit(QtCore.SIGNAL(event), data) else: - print "B", event Registry().execute(event) 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. @@ -505,8 +503,8 @@ class HttpConnection(object): if action == u'search': searches = [] for plugin in self.plugin_manager.plugins: - if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch: - searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])]) + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.hasSearch: + 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}}) @@ -523,8 +521,8 @@ class HttpConnection(object): 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.mediaItem and plugin.mediaItem.hasSearch: - results = plugin.mediaItem.search(text, False) + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) else: results = [] cherrypy.response.headers['Content-Type'] = u'application/json' @@ -539,8 +537,8 @@ class HttpConnection(object): except KeyError, ValueError: return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) - if plugin.status == PluginStatus.Active and plugin.mediaItem: - plugin.mediaItem.goLive(id, remote=True) + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.go_live(id, remote=True) return self._http_success() def add_to_service(self, plugin_name): @@ -552,9 +550,9 @@ class HttpConnection(object): except KeyError, ValueError: return self._http_bad_request() plugin = self.plugin_manager.get_plugin_by_name(type) - if plugin.status == PluginStatus.Active and plugin.mediaItem: - item_id = plugin.mediaItem.createItemFromId(id) - plugin.mediaItem.addToService(item_id, remote=True) + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.add_to_service(item_id, remote=True) self._http_success() def _http_success(self): From 86fb3437599062d4972d6a0c7815ebdee1b7ca2f Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 24 Mar 2013 11:32:03 +0000 Subject: [PATCH 13/71] Added test for SongShowPlus --- .../plugins/songs/lib/songshowplusimport.py | 17 +- .../songs/test_songshowplusimport.py | 169 ++++++++++++++++++ .../openlp_plugins_songs_lib/__init__.py | 0 .../test_songshowplusimport.py | 31 ---- tests/resources/Amazing Grace.sbsong | Bin 0 -> 1018 bytes 5 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 tests/functional/openlp_plugins/songs/test_songshowplusimport.py delete mode 100644 tests/functional/openlp_plugins_songs_lib/__init__.py delete mode 100644 tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py create mode 100644 tests/resources/Amazing Grace.sbsong diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index 84e888856..57a0f3236 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -89,8 +89,9 @@ class SongShowPlusImport(SongImport): * .sbsong """ - otherList = {} - otherCount = 0 + + other_count = 0 + other_list = {} def __init__(self, manager, **kwargs): """ @@ -109,8 +110,8 @@ class SongShowPlusImport(SongImport): if self.stop_import_flag: return self.sspVerseOrderList = [] - other_count = 0 - other_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') @@ -204,11 +205,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/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py new file mode 100644 index 000000000..38962b79d --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -0,0 +1,169 @@ +""" +This module contains tests for the OpenLP song importer. +""" + +import os +from unittest import TestCase +from mock import call, 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')) + +class TestSongShowPlusFileImport(TestCase): + """ + Test the functions in the :mod:`lib` 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 toOpenLPVerseTag_test(self): + """ + Test toOpenLPVerseTag method + """ + # 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.toOpenLPVerseTag(original_tag), openlp_tag, + u'SongShowPlusImport.toOpenLPVerseTag should return "%s" when called with "%s"' + % (openlp_tag, original_tag)) + + # 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.toOpenLPVerseTag(original_tag, ignore_unique=True), openlp_tag, + u'SongShowPlusImport.toOpenLPVerseTag should return "%s" when called with "%s"' + % (openlp_tag, original_tag)) + + + + + def import_source_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() + mocked_import_wizard = MagicMock() + importer = SongShowPlusImport(mocked_manager) + importer.import_wizard = mocked_import_wizard + importer.stop_import_flag = True + + # WHEN: Import source is a string + importer.import_source = u'not a list' + + # 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') + + # WHEN: Import source is an int + importer.import_source = 0 + + # 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') + + # WHEN: Import source is a list + importer.import_source = [u'List', u'of', u'files'] + + # THEN: doImport should return none and the progress bar maximum should be set. + 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 file_import_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() + 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: Import source is a string + importer.import_source = [os.path.join(TEST_PATH, u'Amazing Grace.sbsong')] + + # 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(importer.title, u'Amazing Grace (Demonstration)', + u'Title for Amazing Grace.sbsong should be "Amazing Grace (Demonstration)"') + calls = [call(u'John Newton'), call(u'Edwin Excell'), call(u'John P. Rees')] + mocked_parse_author.assert_has_calls(calls) + mocked_add_copyright.assert_called_with(u'Public Domain ') + self.assertEquals(importer.ccliNumber, 22025, u'ccliNumber should be set as 22025 for Amazing Grace.sbsong') + calls = [call(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'), + call(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'), + call(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'), + call(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'), + call(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')] + mocked_add_verse.assert_has_calls(calls) + self.assertEquals(importer.topics, [u'Assurance', u'Grace', u'Praise', u'Salvation']) + self.assertEquals(importer.comments, u'\n\n\n', u'comments should be "\\n\\n\\n" Amazing Grace.sbsong') + self.assertEquals(importer.songBookName, u'Demonstration Songs', u'songBookName should be ' + u'"Demonstration Songs"') + self.assertEquals(importer.songNumber, 0, u'songNumber should be 0') + self.assertEquals(importer.verseOrderList, [], u'verseOrderList should be empty') + mocked_finish.assert_called_with() \ No newline at end of file diff --git a/tests/functional/openlp_plugins_songs_lib/__init__.py b/tests/functional/openlp_plugins_songs_lib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py b/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py deleted file mode 100644 index 77733a1ba..000000000 --- a/tests/functional/openlp_plugins_songs_lib/test_songshowplusimport.py +++ /dev/null @@ -1,31 +0,0 @@ -""" - Package to test the openlp.plugins.songs.lib package. -""" -import os - -from unittest import TestCase -from mock import MagicMock, patch -from openlp.plugins.songs.lib import songshowplusimport - -TESTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) - - -class TestSongShowPlusImport(TestCase): - -#test do import - # set self.import source to non list type. Do import should return None or False? - # set self.import source to a list of files - # importWizard.progressBar should be set to the number of files in the list - # set self.stop_import_flag to true. Do import should return None or False? - - - - def do_import_test(self): - mocked_manager = MagicMock() - - with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport') as mocked_song_import: - ssp_import_class = songshowplusimport.SongShowPlusImport(mocked_manager) - - songshowplusimport.SongShowPlusImport.importSource = '' - - self.assertEquals(ssp_import_class.SongShowPlusImport().doImport(), False) diff --git a/tests/resources/Amazing Grace.sbsong b/tests/resources/Amazing Grace.sbsong new file mode 100644 index 0000000000000000000000000000000000000000..14b7c35971ad673c98ca9ad8e128e0ad3922444c GIT binary patch literal 1018 zcmZ8g-)j>=5KdZq+S7<2K31nFL?J{8_~cWhwT*}r8_4tKZgRJ{w;Oi%dX4xXC@3QS z&3<#&AL_#;vpcijeDlprzt`(M!k6q#EA>g+f{wh(n4TVR#*FANZ?v30Foz50$#@$t?#ZjWC-u_kj1F9- z5cULjg1FV&!S79p*qKaTOkO51`}lU{u8X*JW!;7)U$Xm#=!j^#q&ql%lYoBm<+8X! zirc4S*HCDfBgK*_xZ39XgLGc1NI{)(PKp}OF)PXFk4zQAJ0oWyOrruB7vhMPbtDTQ zRnbZiUJcR(oT$a-*WMWg=CN@3C0w?WAH%s|v`mm5DWj^3GE%jnl9k8V(F(?BkWOuW z5eTQ;1@de(gW`CQN)>C*nRa!cT<0BH2dviX4q}c1OILfE(MtOeX?Y1CoIVSu?c`jd z-Z`IB32JNaDjlFg;T%96>Iau&9cUpT!qcrG8)voWAVeUGHby+5)NG(1h_9WO*+D`S zBBEiqfNu1PiEZA#6%OBp!;R$Yy!38Jm9iVkl`Ys~R-)4;v}nO9B$GCj=nyI6S>+qb zT*Y88oP*t8k}kdLGzCqCe6fT?tN%1@IUB&BK$HX^q4Qhl>?A)IC0lBEh-6EKiAnJQ zYApyZ6>g*>kmj}5(m>R1WrI*;J65%YZ_y%HM}`Bsq&9Fm3hk!3d?;!wh>b{$9};$1 zuX5fJlwC-YlNRmz#i=r9?Fv7HTNWzWPSX_sy+1Rg B8W{ip literal 0 HcmV?d00001 From c3492d6aedd1ce06f3cb680f8eb3c72ae61b2968 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 24 Mar 2013 13:25:31 +0000 Subject: [PATCH 14/71] Added an extra test song, and cleaned up the test some what --- .../songs/test_songshowplusimport.py | 228 +++++++++++------- .../Beautiful Garden Of Prayer.sbsong | Bin 0 -> 964 bytes 2 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 tests/resources/Beautiful Garden Of Prayer.sbsong diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 38962b79d..4fb1e2479 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -1,5 +1,5 @@ """ -This module contains tests for the OpenLP song importer. +This module contains tests for the SongShow Plus song importer. """ import os @@ -10,10 +10,51 @@ 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')) +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 TestSongShowPlusFileImport(TestCase): + +class TestSongShowPlusImport(TestCase): """ - Test the functions in the :mod:`lib` module. + Test the functions in the :mod:`songshowplusimport` module. """ def create_importer_test(self): """ @@ -29,6 +70,43 @@ class TestSongShowPlusFileImport(TestCase): # THEN: The importer object should not be None self.assertIsNotNone(importer, u'Import should not be none') + def import_source_test(self): + """ + Test SongShowPlusImport.doImport handles different 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 string + importer.import_source = u'not a list' + + # 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') + + # WHEN: Import source is an int + importer.import_source = 0 + + # 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') + + # 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 toOpenLPVerseTag_test(self): """ Test toOpenLPVerseTag method @@ -75,95 +153,69 @@ class TestSongShowPlusFileImport(TestCase): u'SongShowPlusImport.toOpenLPVerseTag should return "%s" when called with "%s"' % (openlp_tag, original_tag)) - - - - def import_source_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() - mocked_import_wizard = MagicMock() - importer = SongShowPlusImport(mocked_manager) - importer.import_wizard = mocked_import_wizard - importer.stop_import_flag = True - - # WHEN: Import source is a string - importer.import_source = u'not a list' - - # 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') - - # WHEN: Import source is an int - importer.import_source = 0 - - # 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') - - # WHEN: Import source is a list - importer.import_source = [u'List', u'of', u'files'] - - # THEN: doImport should return none and the progress bar maximum should be set. - 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 file_import_test(self): """ - Test creating an instance of the SongShow Plus file importer + Test the actual import of real song files and check that the imported data is correct. """ - # GIVEN: A mocked out SongImport class, and a mocked out "manager" + # 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'): - 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 = [] + 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: Import source is a string - importer.import_source = [os.path.join(TEST_PATH, u'Amazing Grace.sbsong')] + # WHEN: Importing each file + importer.import_source = [os.path.join(TEST_PATH, song_file)] + title = SONG_TEST_DATA[song_file][u'title'] + parse_author_calls = [call(author) for author in 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 = \ + [call(verse_text, verse_tag) for verse_text, verse_tag in 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 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(importer.title, u'Amazing Grace (Demonstration)', - u'Title for Amazing Grace.sbsong should be "Amazing Grace (Demonstration)"') - calls = [call(u'John Newton'), call(u'Edwin Excell'), call(u'John P. Rees')] - mocked_parse_author.assert_has_calls(calls) - mocked_add_copyright.assert_called_with(u'Public Domain ') - self.assertEquals(importer.ccliNumber, 22025, u'ccliNumber should be set as 22025 for Amazing Grace.sbsong') - calls = [call(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'), - call(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'), - call(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'), - call(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'), - call(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')] - mocked_add_verse.assert_has_calls(calls) - self.assertEquals(importer.topics, [u'Assurance', u'Grace', u'Praise', u'Salvation']) - self.assertEquals(importer.comments, u'\n\n\n', u'comments should be "\\n\\n\\n" Amazing Grace.sbsong') - self.assertEquals(importer.songBookName, u'Demonstration Songs', u'songBookName should be ' - u'"Demonstration Songs"') - self.assertEquals(importer.songNumber, 0, u'songNumber should be 0') - self.assertEquals(importer.verseOrderList, [], u'verseOrderList should be empty') - mocked_finish.assert_called_with() \ No newline at end of file + # 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)) + mocked_parse_author.assert_has_calls(parse_author_calls) + 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)) + mocked_add_verse.assert_has_calls(add_verse_calls) + 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() + \ No newline at end of file diff --git a/tests/resources/Beautiful Garden Of Prayer.sbsong b/tests/resources/Beautiful Garden Of Prayer.sbsong new file mode 100644 index 0000000000000000000000000000000000000000..c227d48097fb3f8722a874447c39476a80e35828 GIT binary patch literal 964 zcmb7@ZEMs(5XW;@8r#!~puQ9a6zW4frJ(Ptv0m>@3)e5O$xV`l%}&_eT(~czpd#X@ zbS61^DhJgU8)hf}o&C+jj-n_^c*W1(p=yIJS_2=ITcU-F0xq3eql2d@)?|HfDrmCL z9<%!kmMHe$zCL+#60n0Q7yOfjBJ53>Ok^vn4?}H_s?PnnL+P|cHK<+ zfGK|n=_vkou`{it_x{}trr0f)Qf(Tx85iK9FVsr&Kq#3cE~-FtrqrMv+Fq)Fp7pd7 c;kTHNn03M{K1gvz^7zCflOD&z+hjBP1!=b$FaQ7m literal 0 HcmV?d00001 From 401da98d7cf4d3357038a50035e36f57bf89a584 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 24 Mar 2013 14:56:22 +0000 Subject: [PATCH 15/71] increased line lengths to 120 renamed SongShowPlusImport vairables and methods to standards --- .../plugins/songs/lib/songshowplusimport.py | 35 ++++++++----------- .../songs/test_songshowplusimport.py | 11 +++--- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index 57a0f3236..14631ebc2 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -57,31 +57,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. @@ -109,7 +102,7 @@ class SongShowPlusImport(SongImport): for file in self.import_source: if self.stop_import_flag: return - self.sspVerseOrderList = [] + self.ssp_verse_order_list = [] self.other_count = 0 self.other_list = {} file_name = os.path.split(file)[1] @@ -164,27 +157,27 @@ 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): + 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(u'(\D*)(\d+)', verse_name) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 4fb1e2479..9db0ee421 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -109,7 +109,7 @@ class TestSongShowPlusImport(TestCase): def toOpenLPVerseTag_test(self): """ - Test toOpenLPVerseTag method + Test to_openlp_verse_tag method """ # GIVEN: A mocked out SongImport class, and a mocked out "manager" with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'): @@ -130,8 +130,8 @@ class TestSongShowPlusImport(TestCase): # THEN: The returned value should should correlate with the input arguments for original_tag, openlp_tag in test_values: - self.assertEquals(importer.toOpenLPVerseTag(original_tag), openlp_tag, - u'SongShowPlusImport.toOpenLPVerseTag should return "%s" when called with "%s"' + 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)) # WHEN: Supplied with the following arguments replicating a verse order being added @@ -149,8 +149,8 @@ class TestSongShowPlusImport(TestCase): # THEN: The returned value should should correlate with the input arguments for original_tag, openlp_tag in test_values: - self.assertEquals(importer.toOpenLPVerseTag(original_tag, ignore_unique=True), openlp_tag, - u'SongShowPlusImport.toOpenLPVerseTag should return "%s" when called with "%s"' + 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): @@ -218,4 +218,3 @@ class TestSongShowPlusImport(TestCase): self.assertEquals(importer.verseOrderList, [], u'verseOrderList for %s should be %s' % (song_file, verse_order_list)) mocked_finish.assert_called_with() - \ No newline at end of file From 401f5ac2be3b087abfecf4bf6fbbcbca06289d7e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 08:55:05 +0000 Subject: [PATCH 16/71] More updates --- openlp/core/ui/slidecontroller.py | 2 - openlp/plugins/remotes/html/login.html | 3 +- openlp/plugins/remotes/lib/httpauth.py | 10 ++--- openlp/plugins/remotes/lib/httpserver.py | 14 ++----- .../openlp_plugins/remotes/test_auth.py | 38 +++++++++++++++---- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 518e2a715..a66d0057b 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1080,7 +1080,6 @@ class SlideController(DisplayController): """ Go to the next slide. """ - print "next" if not self.service_item: return Registry().execute(u'%s_next' % self.service_item.name.lower(), [self.service_item, self.is_live]) @@ -1108,7 +1107,6 @@ class SlideController(DisplayController): """ Go to the previous slide. """ - print "prev" if not self.service_item: return Registry().execute(u'%s_previous' % self.service_item.name.lower(), [self.service_item, self.is_live]) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 736d3f0ab..5d649629b 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -36,7 +36,6 @@ - @@ -48,4 +47,4 @@ Password:
- \ No newline at end of file + diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index bd3c1f911..e74dc9c04 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -59,11 +59,10 @@ def check_credentials(user_name, password): return translate('RemotePlugin.Mobile', 'Incorrect username or password.') -def check_auth(*args, **kwargs): +def check_authentication(*args, **kwargs): """ - A tool that looks in config for 'auth.require'. If found and it - is not None, a login is required and the entry is evaluated as a list of - conditions that the user must fulfill + A tool that looks in config for 'auth.require'. If found and it is not None, a login is required and the entry is + evaluated as a list of conditions that the user must fulfill """ conditions = cherrypy.request.config.get('auth.require', None) if not Settings().value(u'remotes/authentication enabled'): @@ -79,7 +78,7 @@ def check_auth(*args, **kwargs): else: raise cherrypy.HTTPRedirect("/auth/login") -cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth) +cherrypy.tools.auth = cherrypy.Tool('before_handler', check_authentication) def require_auth(*conditions): @@ -136,6 +135,7 @@ class AuthController(object): """ Provides the actual login control """ + print "login", from_page if username is None or password is None: return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 485a28240..e397d821f 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:: @@ -227,30 +227,24 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) + print "default" self.request_data = None if isinstance(kwargs, dict): self.request_data = kwargs.get(u'data', None) - # Loop through the routes we set up earlier and execute them return self._process_http_request(args, kwargs) @cherrypy.expose def stage(self, *args, **kwargs): """ - Handles the requests for the stage url. This is not secure. + Handles the requests for the /stage url. This is not secure. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) return self._process_http_request(args, kwargs) @cherrypy.expose def files(self, *args, **kwargs): """ - Handles the requests for the files url. This is not secure. + Handles the requests for the /files url. This is not secure. """ - #url = urlparse.urlparse(cherrypy.url()) - #self.url_params = urlparse.parse_qs(url.query) return self._process_http_request(args, kwargs) def _process_http_request(self, args, kwargs): diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py index a300c0127..8afabd0c9 100644 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -4,10 +4,11 @@ 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 mock import patch, MagicMock +import cherrypy from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpauth import check_credentials +from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication from PyQt4 import QtGui __default_settings__ = { @@ -21,6 +22,8 @@ __default_settings__ = { u'remotes/ip address': u'0.0.0.0' } +SESSION_KEY = '_cp_openlp' + class TestLib(TestCase): """ @@ -34,6 +37,10 @@ class TestLib(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) + cherrypy.config.update({'environment': "test_suite"}) + # prevent the HTTP server from ever starting + cherrypy.server.unsubscribe() + cherrypy.engine.start() def tearDown(self): """ @@ -42,24 +49,39 @@ class TestLib(TestCase): del self.application os.unlink(self.ini_file) os.unlink(Settings().fileName()) + cherrypy.engine.exit() def check_credentials_test(self): """ - Test the clean_string() function + Test the Authentication check routine. """ - # GIVEN: A user and password + # GIVEN: A user and password in settings Settings().setValue(u'remotes/user id', u'twinkle') Settings().setValue(u'remotes/password', u'mongoose') - # WHEN: We run the string through the function + # WHEN: We run the function with no input authenticated = check_credentials(u'', u'') - # THEN: The string should be cleaned up and lower-cased + # THEN: The authentication will fail with an error message self.assertEqual(authenticated, u'Incorrect username or password.', u'The return should be a error message string') - # WHEN: We run the string through the function + # WHEN: We run the function with the correct input authenticated = check_credentials(u'twinkle', u'mongoose') - # THEN: The string should be cleaned up and lower-cased + # THEN: The authentication will pass. self.assertEqual(authenticated, None, u'The return should be a None string') + + def check_auth_inactive_test(self): + """ + Test the Authentication check routine. + """ + # GIVEN: A access which is secure + Settings().setValue(u'remotes/authentication enabled', False) + + # WHEN: We run the function with no input + with patch(u'cherrypy.request.config'): + authenticated = check_authentication(None, None) + + # THEN: The authentication will fail with an error message + self.assertEqual(authenticated, None, u'The authentication should return None as not required') From 451189b22202a5075dcadcf714e4d3c6b9067630 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 11:39:32 +0000 Subject: [PATCH 17/71] Fix up login page to handle redirection correctly --- openlp/plugins/remotes/html/login.html | 2 -- openlp/plugins/remotes/lib/httpauth.py | 4 +--- openlp/plugins/remotes/lib/httpserver.py | 8 +++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html index 5d649629b..60c71166c 100644 --- a/openlp/plugins/remotes/html/login.html +++ b/openlp/plugins/remotes/html/login.html @@ -35,8 +35,6 @@ - -
diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index e74dc9c04..cfa14a4a1 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -52,7 +52,6 @@ def check_credentials(user_name, password): Verifies credentials for username and password. Returns None on success or a string describing the error on failure """ - print "check" if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): return None else: @@ -128,6 +127,7 @@ class AuthController(object): login_html = os.path.normpath(os.path.join(directory, u'login.html')) html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) cherrypy.response.headers['Content-Type'] = u'text/html' + cherrypy.response.status = 200 return html @cherrypy.expose @@ -135,7 +135,6 @@ class AuthController(object): """ Provides the actual login control """ - print "login", from_page if username is None or password is None: return self.get_login_form("", from_page=from_page) error_msg = check_credentials(username, password) @@ -144,7 +143,6 @@ class AuthController(object): else: cherrypy.session[SESSION_KEY] = cherrypy.request.login = username self.on_login(username) - print from_page raise cherrypy.HTTPRedirect(from_page or "/") @cherrypy.expose diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index e397d821f..6759d10f3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -227,7 +227,6 @@ class HttpConnection(object): """ Handles the requests for the main url. This is secure depending on settings in config. """ - print "default" self.request_data = None if isinstance(kwargs, dict): self.request_data = kwargs.get(u'data', None) @@ -252,7 +251,6 @@ class HttpConnection(object): Common function to process HTTP requests where secure or insecure """ url = urlparse.urlparse(cherrypy.url()) - #self.url_params = kwargs response = None for route, func in self.routes: match = re.match(route, url.path) @@ -315,7 +313,11 @@ class HttpConnection(object): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides') + 'slides': translate('RemotePlugin.Mobile', 'Slides'), + 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), + 'from_page': "", + 'message': "", + 'username': "username" } def serve_file(self, filename=None): From 3fef841bb27afcd3e7c4c4dfb2015be9fa3e0324 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 26 Mar 2013 12:45:18 +0100 Subject: [PATCH 18/71] fixed FTW shown again --- openlp/core/__init__.py | 2 ++ openlp/core/ui/mainwindow.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index dd2a3d18c..23986ffc4 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -305,6 +305,8 @@ 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'core/has run wizard'): if not FirstTimeLanguageForm().exec_(): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 8f61d595c..6bcad24c9 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -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() From ab011a22c195ec0781e57b38f97de3ab3538d3e1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Thu, 28 Mar 2013 14:45:47 +0000 Subject: [PATCH 19/71] last version before big refactor --- openlp/plugins/remotes/lib/httpauth.py | 2 ++ openlp/plugins/remotes/lib/httpserver.py | 6 ++---- openlp/plugins/remotes/remoteplugin.py | 1 + tests/functional/openlp_plugins/remotes/test_auth.py | 12 +++--------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py index cfa14a4a1..f760bfc8c 100644 --- a/openlp/plugins/remotes/lib/httpauth.py +++ b/openlp/plugins/remotes/lib/httpauth.py @@ -64,6 +64,8 @@ def check_authentication(*args, **kwargs): evaluated as a list of conditions that the user must fulfill """ conditions = cherrypy.request.config.get('auth.require', None) + a = cherrypy.request + print a if not Settings().value(u'remotes/authentication enabled'): return None if conditions is not None: diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 6759d10f3..22a75e49e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -138,7 +138,7 @@ class HttpServer(object): def __init__(self, plugin): """ - Initialise the httpserver, and start the server. + Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') self.plugin = plugin @@ -146,13 +146,11 @@ class HttpServer(object): self.connections = [] self.conf = {'/files': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.html_dir}} - self.start_server() 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. + Listen out for slide and song changes so they can be broadcast to clients. Listen out for socket connections. """ log.debug(u'Start CherryPy server') if Settings().value(self.plugin.settings_section + u'/https enabled'): diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 4e37f9853..5be537b60 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -66,6 +66,7 @@ class RemotesPlugin(Plugin): log.debug(u'initialise') Plugin.initialise(self) self.server = HttpServer(self) + self.server.start_server() def finalise(self): """ diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py index 8afabd0c9..32622af9c 100644 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ b/tests/functional/openlp_plugins/remotes/test_auth.py @@ -5,7 +5,6 @@ import os from unittest import TestCase from tempfile import mkstemp from mock import patch, MagicMock -import cherrypy from openlp.core.lib import Settings from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication @@ -25,7 +24,7 @@ __default_settings__ = { SESSION_KEY = '_cp_openlp' -class TestLib(TestCase): +class TestAuth(TestCase): """ Test the functions in the :mod:`lib` module. """ @@ -37,10 +36,6 @@ class TestLib(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) - cherrypy.config.update({'environment': "test_suite"}) - # prevent the HTTP server from ever starting - cherrypy.server.unsubscribe() - cherrypy.engine.start() def tearDown(self): """ @@ -49,11 +44,10 @@ class TestLib(TestCase): del self.application os.unlink(self.ini_file) os.unlink(Settings().fileName()) - cherrypy.engine.exit() def check_credentials_test(self): """ - Test the Authentication check routine. + Test the Authentication check routine with credentials. """ # GIVEN: A user and password in settings Settings().setValue(u'remotes/user id', u'twinkle') @@ -74,7 +68,7 @@ class TestLib(TestCase): def check_auth_inactive_test(self): """ - Test the Authentication check routine. + Test the Authentication check routine when inactive. """ # GIVEN: A access which is secure Settings().setValue(u'remotes/authentication enabled', False) From 4d0f9f3f914221852870b509323bbe792caf2954 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Thu, 28 Mar 2013 19:50:06 +0000 Subject: [PATCH 20/71] Tided up the test somewhat --- .../songs/test_songshowplusimport.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 9db0ee421..0fae3fd56 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -88,7 +88,15 @@ class TestSongShowPlusImport(TestCase): # 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') + u'setMaxium on import_wizard.progress_bar should not have been called') + + # 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 an int importer.import_source = 0 @@ -96,7 +104,15 @@ class TestSongShowPlusImport(TestCase): # 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') + u'setMaxium on import_wizard.progress_bar should not have been called') + + # 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'] @@ -104,10 +120,10 @@ class TestSongShowPlusImport(TestCase): # 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') + 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 toOpenLPVerseTag_test(self): + def to_openlp_verse_tag_test(self): """ Test to_openlp_verse_tag method """ @@ -134,6 +150,11 @@ class TestSongShowPlusImport(TestCase): u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' % (openlp_tag, original_tag)) + # 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'), From bc032eb7e247cc18ff39c4481060dbf9518a69d1 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Thu, 28 Mar 2013 19:56:50 +0000 Subject: [PATCH 21/71] fixed indentation --- .../functional/openlp_plugins/songs/test_songshowplusimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 0fae3fd56..aa2753485 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -106,7 +106,7 @@ class TestSongShowPlusImport(TestCase): self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False, u'setMaxium on import_wizard.progress_bar should not have been called') - # GIVEN: A mocked out SongImport class, and a mocked out "manager" + # 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() From 2920eecd314298c4450d52741afd455620119801 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Thu, 28 Mar 2013 21:09:38 +0000 Subject: [PATCH 22/71] Moved sample song files to their own directory --- .../{ => songshowplussongs}/Amazing Grace.sbsong | Bin .../Beautiful Garden Of Prayer.sbsong | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/resources/{ => songshowplussongs}/Amazing Grace.sbsong (100%) rename tests/resources/{ => songshowplussongs}/Beautiful Garden Of Prayer.sbsong (100%) diff --git a/tests/resources/Amazing Grace.sbsong b/tests/resources/songshowplussongs/Amazing Grace.sbsong similarity index 100% rename from tests/resources/Amazing Grace.sbsong rename to tests/resources/songshowplussongs/Amazing Grace.sbsong diff --git a/tests/resources/Beautiful Garden Of Prayer.sbsong b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong similarity index 100% rename from tests/resources/Beautiful Garden Of Prayer.sbsong rename to tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong From 86f294e8707c59875eb4875b4aad4b9c22b7884a Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Thu, 28 Mar 2013 21:17:07 +0000 Subject: [PATCH 23/71] Changed path in test file --- .../functional/openlp_plugins/songs/test_songshowplusimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index aa2753485..24c77d0f3 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -9,7 +9,7 @@ from mock import call, 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')) +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'], From 8cf886971c1b5f1c9007a5e92d0cdd44c4897878 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 07:48:55 +0000 Subject: [PATCH 24/71] Refactor http server --- openlp/core/ui/settingsform.py | 1 + openlp/plugins/remotes/lib/httpserver.py | 150 ++++++++++++++--------- openlp/plugins/remotes/lib/remotetab.py | 24 +++- openlp/plugins/remotes/remoteplugin.py | 8 +- 4 files changed, 114 insertions(+), 69 deletions(-) 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/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 22a75e49e..b660ec094 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -126,54 +126,115 @@ from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.utils import AppLocation, translate -from openlp.plugins.remotes.lib.httpauth import AuthController, require_auth + +from cherrypy._cpcompat import md5, sha, ntob log = logging.getLogger(__name__) +def sha_password_encrypter(password): + + return sha(ntob(password)).hexdigest() + + +def fetch_password(username): + if username != Settings().value(u'remotes/user id'): + return None + print "fetch password", username + return sha(ntob(Settings().value(u'remotes/password'))).hexdigest() + + class HttpServer(object): """ Ability to control OpenLP via a web browser. """ + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + def __init__(self, plugin): """ 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.conf = {'/files': {u'tools.staticdir.on': True, - u'tools.staticdir.dir': self.html_dir}} + self.router = HttpRouter() 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 CherryPy server') + # Define to security levels and add 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 define_config(self): if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') shared_data = AppLocation.get_directory(AppLocation.SharedData) - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port, - u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), - u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')} + cherrypy.config.update({u'server.socket_host': str(address), + u'server.socket_port': port, + u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'), + u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')}) else: port = Settings().value(self.plugin.settings_section + u'/port') address = Settings().value(self.plugin.settings_section + u'/ip address') - server_config = {u'server.socket_host': str(address), - u'server.socket_port': port} - cherrypy.config.update(server_config) - cherrypy.config.update({'environment': 'embedded'}) - cherrypy.config.update({'engine.autoreload_on': False}) - cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf) - # Turn off the flood of access messages cause by poll - cherrypy.log.access_log.propagate = False - cherrypy.engine.start() - log.debug(u'TCP listening on port %d' % port) + 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': sha_password_encrypter}, + 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 reload_config(self): + cherrypy.tree.mount(self.root, '/', config=self.define_config()) + cherrypy.config.reset() + + class Public: + @cherrypy.expose + def default(self, *args, **kwargs): + print "public" + 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: + @cherrypy.expose + def default(self, *args, **kwargs): + print "files" + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Stage: + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) def close(self): """ @@ -181,25 +242,17 @@ class HttpServer(object): """ log.debug(u'close http server') cherrypy.engine.exit() - cherrypy.engine.stop() -class HttpConnection(object): +class HttpRouter(object): """ A single connection, this handles communication between the server and the client. """ - _cp_config = { - 'tools.sessions.on': True, - 'tools.auth.on': True - } - auth = AuthController() - - def __init__(self, parent): + def __init__(self): """ Initialise the CherryPy Server """ - self.parent = parent self.routes = [ (u'^/$', self.serve_file), (u'^/(stage)$', self.serve_file), @@ -218,33 +271,9 @@ class HttpConnection(object): (r'^/api/(.*)/add$', self.add_to_service) ] self.translate() + self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - @cherrypy.expose - @require_auth() - def default(self, *args, **kwargs): - """ - Handles the requests for the main url. This is secure depending on settings in config. - """ - self.request_data = None - if isinstance(kwargs, dict): - self.request_data = kwargs.get(u'data', None) - return self._process_http_request(args, kwargs) - - @cherrypy.expose - def stage(self, *args, **kwargs): - """ - Handles the requests for the /stage url. This is not secure. - """ - return self._process_http_request(args, kwargs) - - @cherrypy.expose - def files(self, *args, **kwargs): - """ - Handles the requests for the /files url. This is not secure. - """ - return self._process_http_request(args, kwargs) - - def _process_http_request(self, args, kwargs): + def process_http_request(self, url_path, *args): """ Common function to process HTTP requests where secure or insecure """ @@ -253,7 +282,7 @@ class HttpConnection(object): for route, func in self.routes: match = re.match(route, url.path) if match: - log.debug('Route "%s" matched "%s"', route, url.path) + log.debug('Route "%s" matched "%s"', route, url_path) args = [] for param in match.groups(): args.append(param) @@ -332,8 +361,8 @@ 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): + 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 @@ -450,7 +479,6 @@ class HttpConnection(object): if current_item: json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier else: - print event if self.request_data: try: data = json.loads(self.request_data)[u'request'][u'id'] diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 7c76516dc..77baf9116 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -155,6 +155,7 @@ class RemoteTab(SettingsTab): 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')) @@ -224,15 +225,22 @@ class RemoteTab(SettingsTab): 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() 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(): - changed = True + self.https_settings_group_box.isChecked() or \ + Settings().value(self.settings_section + u'/authentication enabled') != \ + self.user_login_group_box.isChecked() or \ + Settings().value(self.settings_section + u'/user id') != self.user_id.text() or \ + Settings().value(self.settings_section + u'/password') != self.password.text(): + 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()) @@ -241,12 +249,16 @@ class RemoteTab(SettingsTab): 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()) - if changed: - Registry().execute(u'remotes_config_updated') - def on_twelve_hour_check_box_changed(self, check_state): 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 5be537b60..fd2906feb 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 @@ -76,6 +78,7 @@ class RemotesPlugin(Plugin): Plugin.finalise(self) if self.server: self.server.close() + self.server = None def about(self): """ @@ -105,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.')) From 347636d0787240d54a72705a6cdbcabdcc0fd434 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 08:33:10 +0000 Subject: [PATCH 25/71] Move userid and password change out of restart scope --- openlp/plugins/remotes/lib/remotetab.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 77baf9116..658f5cfcf 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -31,7 +31,7 @@ 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 @@ -237,9 +237,7 @@ class RemoteTab(SettingsTab): 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() or \ - Settings().value(self.settings_section + u'/user id') != self.user_id.text() or \ - Settings().value(self.settings_section + u'/password') != self.password.text(): + 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()) From ced77e4ec3f236779fe0b9cd17e79700a0cafae2 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:06:43 +0000 Subject: [PATCH 26/71] Add some proper tests --- openlp/plugins/remotes/lib/httpauth.py | 162 ------------------ openlp/plugins/remotes/lib/httpserver.py | 3 +- .../openlp_plugins/remotes/test_auth.py | 81 --------- .../{test_server.py => test_router.py} | 32 ++-- 4 files changed, 18 insertions(+), 260 deletions(-) delete mode 100644 openlp/plugins/remotes/lib/httpauth.py delete mode 100644 tests/functional/openlp_plugins/remotes/test_auth.py rename tests/functional/openlp_plugins/remotes/{test_server.py => test_router.py} (71%) diff --git a/openlp/plugins/remotes/lib/httpauth.py b/openlp/plugins/remotes/lib/httpauth.py deleted file mode 100644 index f760bfc8c..000000000 --- a/openlp/plugins/remotes/lib/httpauth.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2013 Raoul Snyman # -# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # -# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # -# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # -# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # -# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### - -""" -The :mod:`http` module manages the HTTP authorisation logic. This code originates from -http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions - -""" - -import cherrypy -import logging -import os - -from mako.template import Template - -from openlp.core.lib import Settings -from openlp.core.utils import AppLocation, translate - -SESSION_KEY = '_cp_openlp' - -log = logging.getLogger(__name__) - - -def check_credentials(user_name, password): - """ - Verifies credentials for username and password. - Returns None on success or a string describing the error on failure - """ - if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'): - return None - else: - return translate('RemotePlugin.Mobile', 'Incorrect username or password.') - - -def check_authentication(*args, **kwargs): - """ - A tool that looks in config for 'auth.require'. If found and it is not None, a login is required and the entry is - evaluated as a list of conditions that the user must fulfill - """ - conditions = cherrypy.request.config.get('auth.require', None) - a = cherrypy.request - print a - if not Settings().value(u'remotes/authentication enabled'): - return None - if conditions is not None: - username = cherrypy.session.get(SESSION_KEY) - if username: - cherrypy.request.login = username - for condition in conditions: - # A condition is just a callable that returns true or false - if not condition(): - raise cherrypy.HTTPRedirect("/auth/login") - else: - raise cherrypy.HTTPRedirect("/auth/login") - -cherrypy.tools.auth = cherrypy.Tool('before_handler', check_authentication) - - -def require_auth(*conditions): - """ - A decorator that appends conditions to the auth.require config variable. - """ - def decorate(f): - """ - Lets process a decoration. - """ - if not hasattr(f, '_cp_config'): - f._cp_config = dict() - if 'auth.require' not in f._cp_config: - f._cp_config['auth.require'] = [] - f._cp_config['auth.require'].extend(conditions) - return f - return decorate - - -class AuthController(object): - - def on_login(self, username): - """ - Called on successful login - """ - pass - - def on_logout(self, username): - """ - Called on logout - """ - pass - - def get_login_form(self, username, message=None, from_page="/"): - """ - Provides a login form - """ - if not message: - message = translate('RemotePlugin.Mobile', 'Enter login information') - variables = { - 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), - 'from_page': from_page, - 'message': message, - 'username': username - } - directory = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - login_html = os.path.normpath(os.path.join(directory, u'login.html')) - html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables) - cherrypy.response.headers['Content-Type'] = u'text/html' - cherrypy.response.status = 200 - return html - - @cherrypy.expose - def login(self, username=None, password=None, from_page="/"): - """ - Provides the actual login control - """ - if username is None or password is None: - return self.get_login_form("", from_page=from_page) - error_msg = check_credentials(username, password) - if error_msg: - return self.get_login_form(username, from_page, error_msg,) - else: - cherrypy.session[SESSION_KEY] = cherrypy.request.login = username - self.on_login(username) - raise cherrypy.HTTPRedirect(from_page or "/") - - @cherrypy.expose - def logout(self, from_page="/"): - """ - Provides the actual logout functions - """ - sess = cherrypy.session - username = sess.get(SESSION_KEY, None) - sess[SESSION_KEY] = None - if username: - cherrypy.request.login = None - self.on_logout(username) - raise cherrypy.HTTPRedirect(from_page or "/") - diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index b660ec094..f8a79899e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -277,10 +277,9 @@ class HttpRouter(object): """ Common function to process HTTP requests where secure or insecure """ - url = urlparse.urlparse(cherrypy.url()) response = None for route, func in self.routes: - match = re.match(route, url.path) + match = re.match(route, url_path) if match: log.debug('Route "%s" matched "%s"', route, url_path) args = [] diff --git a/tests/functional/openlp_plugins/remotes/test_auth.py b/tests/functional/openlp_plugins/remotes/test_auth.py deleted file mode 100644 index 32622af9c..000000000 --- a/tests/functional/openlp_plugins/remotes/test_auth.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -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, MagicMock - -from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication -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' -} - -SESSION_KEY = '_cp_openlp' - - -class TestAuth(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__) - - 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) - os.unlink(Settings().fileName()) - - def check_credentials_test(self): - """ - Test the Authentication check routine with credentials. - """ - # GIVEN: A user and password in settings - Settings().setValue(u'remotes/user id', u'twinkle') - Settings().setValue(u'remotes/password', u'mongoose') - - # WHEN: We run the function with no input - authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, u'Incorrect username or password.', - u'The return should be a error message string') - - # WHEN: We run the function with the correct input - authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - self.assertEqual(authenticated, None, u'The return should be a None string') - - def check_auth_inactive_test(self): - """ - Test the Authentication check routine when inactive. - """ - # GIVEN: A access which is secure - Settings().setValue(u'remotes/authentication enabled', False) - - # WHEN: We run the function with no input - with patch(u'cherrypy.request.config'): - authenticated = check_authentication(None, None) - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, None, u'The authentication should return None as not required') diff --git a/tests/functional/openlp_plugins/remotes/test_server.py b/tests/functional/openlp_plugins/remotes/test_router.py similarity index 71% rename from tests/functional/openlp_plugins/remotes/test_server.py rename to tests/functional/openlp_plugins/remotes/test_router.py index d574a8542..9604bc747 100644 --- a/tests/functional/openlp_plugins/remotes/test_server.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -6,10 +6,9 @@ import os from unittest import TestCase from tempfile import mkstemp from mock import patch, MagicMock -import cherrypy from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpConnection +from openlp.plugins.remotes.lib.httpserver import HttpRouter from PyQt4 import QtGui __default_settings__ = { @@ -23,10 +22,8 @@ __default_settings__ = { u'remotes/ip address': u'0.0.0.0' } -SESSION_KEY = '_cp_openlp' - -class TestAuth(TestCase): +class TestRouter(TestCase): """ Test the functions in the :mod:`lib` module. """ @@ -38,7 +35,7 @@ class TestAuth(TestCase): Settings().set_filename(self.ini_file) self.application = QtGui.QApplication.instance() Settings().extend_default_settings(__default_settings__) - self.server = HttpConnection(None) + self.router = HttpRouter() def tearDown(self): """ @@ -49,19 +46,24 @@ class TestAuth(TestCase): def process_http_request_test(self): """ - Test the Authentication check routine with credentials. + Test the router control functionality """ - # GIVEN: A user and password in settings - cherrypy = MagicMock() - cherrypy.url.return_value = "nosetest/apl/poll" + # GIVEN: A testing set of Routes + mocked_function = MagicMock() + test_route = [ + (r'^/stage/api/poll$', mocked_function), + ] + self.router.routes = test_route - print cherrypy.url() + # WHEN: called with a poll route + self.router.process_http_request(u'/stage/api/poll', None) - with patch(u'url.path') as mocked_url: - mocked_url.return_value = "nosetest/apl/poll" - self.server._process_http_request(None, 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.' - self.assertFalse() + + #self.assertFalse() # WHEN: We run the function with no input #authenticated = check_credentials(u'', u'') From 6cd384e02824111db4a72a4fe7275193c52beec6 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:32:46 +0000 Subject: [PATCH 27/71] Fix up tests --- openlp/plugins/remotes/lib/httpserver.py | 41 +++++---- .../openlp_plugins/remotes/test_router.py | 58 ++++++++----- .../remotes/test_remoteserver.py | 85 ------------------- 3 files changed, 63 insertions(+), 121 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/remotes/test_remoteserver.py diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index f8a79899e..5a9e05042 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -127,28 +127,32 @@ from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.utils import AppLocation, translate -from cherrypy._cpcompat import md5, sha, ntob +from cherrypy._cpcompat import sha, ntob log = logging.getLogger(__name__) def sha_password_encrypter(password): - + """ + Create an encrypted password for the given password. + """ return sha(ntob(password)).hexdigest() def fetch_password(username): + """ + Fetch the password for a provided user. + """ if username != Settings().value(u'remotes/user id'): return None - print "fetch password", username - return sha(ntob(Settings().value(u'remotes/password'))).hexdigest() + return sha_password_encrypter(Settings().value(u'remotes/password')) class HttpServer(object): """ Ability to control OpenLP via a web browser. + This class controls the Cherrypy server and configuration. """ - _cp_config = { 'tools.sessions.on': True, 'tools.auth.on': True @@ -167,7 +171,7 @@ class HttpServer(object): Start the http server based on configuration. """ log.debug(u'Start CherryPy server') - # Define to security levels and add the router code + # Define to security levels and inject the router code self.root = self.Public() self.root.files = self.Files() self.root.stage = self.Stage() @@ -180,6 +184,9 @@ class HttpServer(object): cherrypy.engine.start() def define_config(self): + """ + Define the configuration of the server. + """ if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') @@ -209,14 +216,12 @@ class HttpServer(object): u'tools.basic_auth.on': False}} return directory_config - def reload_config(self): - cherrypy.tree.mount(self.root, '/', config=self.define_config()) - cherrypy.config.reset() - class Public: + """ + Main access class with may have security enabled on it. + """ @cherrypy.expose def default(self, *args, **kwargs): - print "public" self.router.request_data = None if isinstance(kwargs, dict): self.router.request_data = kwargs.get(u'data', None) @@ -224,13 +229,18 @@ class HttpServer(object): return self.router.process_http_request(url.path, *args) class Files: + """ + Provides access to files and has no security available. These are read only accesses + """ @cherrypy.expose def default(self, *args, **kwargs): - print "files" url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) class Stage: + """ + 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()) @@ -246,12 +256,11 @@ class HttpServer(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): """ - Initialise the CherryPy Server + Initialise the router """ self.routes = [ (u'^/$', self.serve_file), @@ -275,7 +284,7 @@ class HttpRouter(object): def process_http_request(self, url_path, *args): """ - Common function to process HTTP requests where secure or insecure + Common function to process HTTP requests """ response = None for route, func in self.routes: diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 9604bc747..f86b69612 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -8,7 +8,7 @@ from tempfile import mkstemp from mock import patch, MagicMock from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpRouter +from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter from PyQt4 import QtGui __default_settings__ = { @@ -44,6 +44,43 @@ class TestRouter(TestCase): 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') + print password + + # 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 = sha_password_encrypter(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 = sha_password_encrypter(u'password') + test_value = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' + + # THEN: the function should return the correct password + self.assertEqual(required_password, test_value, + u'The result for sha_password_encrypter should return the correct encrypted password') + def process_http_request_test(self): """ Test the router control functionality @@ -61,22 +98,3 @@ class TestRouter(TestCase): # 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.' - - - #self.assertFalse() - - # WHEN: We run the function with no input - #authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - #self.assertEqual(authenticated, u'Incorrect username or password.', - # u'The return should be a error message string') - - # WHEN: We run the function with the correct input - #authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - #self.assertEqual(authenticated, None, u'The return should be a None string') - - - diff --git a/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py b/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py deleted file mode 100644 index ded7ca4c9..000000000 --- a/tests/interfaces/openlp_plugins/remotes/test_remoteserver.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -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, MagicMock - - -import urllib -from BeautifulSoup import BeautifulSoup, NavigableString, Tag - -from openlp.core.lib import Settings -from openlp.plugins.remotes.lib 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' -} - -SESSION_KEY = '_cp_openlp' - - -class TestRemoteServer(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(self) - - 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) - os.unlink(Settings().fileName()) - self.server.close() - - def check_access_test(self): - """ - Test the Authentication check routine. - """ - # GIVEN: A user and password in settings - Settings().setValue(u'remotes/user id', u'twinkle') - Settings().setValue(u'remotes/password', u'mongoose') - - # WHEN: We run the function with no input - authenticated = check_credentials(u'', u'') - - # THEN: The authentication will fail with an error message - self.assertEqual(authenticated, u'Incorrect username or password.', - u'The return should be a error message string') - - # WHEN: We run the function with the correct input - authenticated = check_credentials(u'twinkle', u'mongoose') - - # THEN: The authentication will pass. - self.assertEqual(authenticated, None, u'The return should be a None string') - - def check_auth_inactive_test(self): - """ - Test the Authentication check routine. - """ - # GIVEN: A access which is secure - Settings().setValue(u'remotes/authentication enabled', True) - - # WHEN: We run the function with no input - f = urllib.urlopen("http://localhost:4316") - soup = BeautifulSoup(f.read()) - print soup.title.string From eb51590becf22cb7930fb6ebef049bcaa817697a Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 09:39:41 +0000 Subject: [PATCH 28/71] Code cleanup ready for reviews --- openlp/plugins/remotes/html/login.css | 46 ----------------------- openlp/plugins/remotes/html/login.html | 48 ------------------------ openlp/plugins/remotes/lib/httpserver.py | 15 +++++--- openlp/plugins/remotes/lib/remotetab.py | 9 +++++ 4 files changed, 19 insertions(+), 99 deletions(-) delete mode 100644 openlp/plugins/remotes/html/login.css delete mode 100644 openlp/plugins/remotes/html/login.html diff --git a/openlp/plugins/remotes/html/login.css b/openlp/plugins/remotes/html/login.css deleted file mode 100644 index 3881b55ee..000000000 --- a/openlp/plugins/remotes/html/login.css +++ /dev/null @@ -1,46 +0,0 @@ -/****************************************************************************** -* OpenLP - Open Source Lyrics Projection * -* --------------------------------------------------------------------------- * -* Copyright (c) 2008-2013 Raoul Snyman * -* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan * -* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, * -* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. * -* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, * -* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, * -* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, * -* Frode Woldsund, Martin Zibricky * -* --------------------------------------------------------------------------- * -* This program is free software; you can redistribute it and/or modify it * -* under the terms of the GNU General Public License as published by the Free * -* Software Foundation; version 2 of the License. * -* * -* This program is distributed in the hope that it will be useful, but WITHOUT * -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * -* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * -* more details. * -* * -* You should have received a copy of the GNU General Public License along * -* with this program; if not, write to the Free Software Foundation, Inc., 59 * -* Temple Place, Suite 330, Boston, MA 02111-1307 USA * -******************************************************************************/ - -.ui-icon-blank { - background-image: url(images/ui-icon-blank.png); -} - -.ui-icon-unblank { - background-image: url(images/ui-icon-unblank.png); -} - -/* Overwrite style from jquery-mobile.css */ -.ui-li .ui-btn-text a.ui-link-inherit{ - white-space: normal; -} - -.ui-page{ - padding: 100px 100px 100px 100px; - width: 300px; -} -.ui-input-text{ - width: 30px; -} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/login.html b/openlp/plugins/remotes/html/login.html deleted file mode 100644 index 60c71166c..000000000 --- a/openlp/plugins/remotes/html/login.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - ${title} - - - - - - - -

${message}

-

- User name:
- Password:
- - - diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 5a9e05042..677ea3146 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -348,11 +348,7 @@ class HttpRouter(object): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides'), - 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'), - 'from_page': "", - 'message': "", - 'username': "username" + 'slides': translate('RemotePlugin.Mobile', 'Slides') } def serve_file(self, filename=None): @@ -588,12 +584,21 @@ class HttpRouter(object): self._http_success() def _http_success(self): + """ + Set the HTTP success return code. + """ cherrypy.response.status = 200 def _http_bad_request(self): + """ + Set the HTTP bad response return code. + """ cherrypy.response.status = 400 def _http_not_found(self): + """ + Set the HTTP not found return code. + """ cherrypy.response.status = 404 cherrypy.response.body = ["Sorry, an error occurred "] diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 658f5cfcf..685cd5882 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -181,6 +181,9 @@ class RemoteTab(SettingsTab): 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() @@ -206,6 +209,9 @@ class RemoteTab(SettingsTab): 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')) @@ -249,6 +255,9 @@ class RemoteTab(SettingsTab): Settings().setValue(self.settings_section + u'/password', self.password.text()) 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: From efa7d8c8c0039ba6e4c5e777398f214f8b686adb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 29 Mar 2013 20:58:06 +0000 Subject: [PATCH 29/71] minor fixes and move certificate location --- openlp/core/utils/applocation.py | 7 +++---- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- openlp/plugins/remotes/lib/remotetab.py | 6 +++--- tests/functional/openlp_plugins/remotes/__init__.py | 1 - tests/functional/openlp_plugins/remotes/test_router.py | 5 ++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/openlp/core/utils/applocation.py b/openlp/core/utils/applocation.py index 4215cad22..2f1cb45ba 100644 --- a/openlp/core/utils/applocation.py +++ b/openlp/core/utils/applocation.py @@ -63,7 +63,6 @@ class AppLocation(object): VersionDir = 5 CacheDir = 6 LanguageDir = 7 - SharedData = 8 # Base path where data/config/cache dir is located BaseDir = None @@ -150,18 +149,18 @@ def _get_os_dir_path(dir_type): if sys.platform == u'win32': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp', u'data') - elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + elif dir_type == AppLocation.LanguageDir: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp') elif sys.platform == u'darwin': if dir_type == AppLocation.DataDir: return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp', u'Data') - elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + elif dir_type == AppLocation.LanguageDir: return os.path.split(openlp.__file__)[0] return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp') else: - if dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData: + if dir_type == AppLocation.LanguageDir: prefixes = [u'/usr/local', u'/usr'] for prefix in prefixes: directory = os.path.join(prefix, u'share', u'openlp') diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 677ea3146..51490bb38 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -190,11 +190,11 @@ class HttpServer(object): if Settings().value(self.plugin.settings_section + u'/https enabled'): port = Settings().value(self.plugin.settings_section + u'/https port') address = Settings().value(self.plugin.settings_section + u'/ip address') - shared_data = AppLocation.get_directory(AppLocation.SharedData) + 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(shared_data, u'openlp.crt'), - u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')}) + 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.plugin.settings_section + u'/port') address = Settings().value(self.plugin.settings_section + u'/ip address') diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 685cd5882..09934b58c 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -217,9 +217,9 @@ class RemoteTab(SettingsTab): 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) - shared_data = AppLocation.get_directory(AppLocation.SharedData) - if not os.path.exists(os.path.join(shared_data, u'openlp.crt')) or \ - not os.path.exists(os.path.join(shared_data, u'openlp.key')): + 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) diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py index f87606f07..e69de29bb 100644 --- a/tests/functional/openlp_plugins/remotes/__init__.py +++ b/tests/functional/openlp_plugins/remotes/__init__.py @@ -1 +0,0 @@ -__author__ = 'tim' diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index f86b69612..3b344c3b2 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -5,7 +5,7 @@ import os from unittest import TestCase from tempfile import mkstemp -from mock import patch, MagicMock +from mock import MagicMock from openlp.core.lib import Settings from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter @@ -51,7 +51,6 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid password = fetch_password(u'itwinkle') - print password # THEN: the function should return None self.assertEqual(password, None, u'The result for fetch_password should be None') @@ -75,7 +74,7 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid required_password = sha_password_encrypter(u'password') - test_value = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' + test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' # THEN: the function should return the correct password self.assertEqual(required_password, test_value, From 25d74260ce72f659edb5a6504d29b684f793e79b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 30 Mar 2013 06:56:28 +0000 Subject: [PATCH 30/71] Add tests for the server --- openlp/plugins/bibles/lib/http.py | 3 + openlp/plugins/remotes/lib/httpserver.py | 14 +- openlp/plugins/remotes/remoteplugin.py | 2 +- .../openlp_core_lib/test_uistrings.py | 1 + tests/interfaces/openlp_plugins/__init__.pyc | Bin 184 -> 0 bytes .../openlp_plugins/remotes/test_server.py | 120 ++++++++++++++++++ 6 files changed, 132 insertions(+), 8 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/__init__.pyc create mode 100644 tests/interfaces/openlp_plugins/remotes/test_server.py diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index b01377a05..736727e20 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -55,6 +55,7 @@ UGLY_CHARS = { log = logging.getLogger(__name__) + class BGExtract(object): """ Extract verses from BibleGateway @@ -671,6 +672,7 @@ class HTTPBible(BibleDB): application = property(_get_application) + def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None, cleaner=None): """ @@ -715,6 +717,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, 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/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 51490bb38..0489428ac 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -158,12 +158,12 @@ class HttpServer(object): 'tools.auth.on': True } - def __init__(self, plugin): + def __init__(self): """ Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') - self.plugin = plugin + self.settings_section = u'remotes' self.router = HttpRouter() def start_server(self): @@ -187,17 +187,17 @@ class HttpServer(object): """ Define the configuration of the server. """ - if Settings().value(self.plugin.settings_section + u'/https enabled'): - port = Settings().value(self.plugin.settings_section + u'/https port') - address = Settings().value(self.plugin.settings_section + u'/ip address') + 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.plugin.settings_section + u'/port') - address = Settings().value(self.plugin.settings_section + u'/ip address') + 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'}) diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index fd2906feb..f443fbda4 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -67,7 +67,7 @@ class RemotesPlugin(Plugin): """ log.debug(u'initialise') Plugin.initialise(self) - self.server = HttpServer(self) + self.server = HttpServer() self.server.start_server() def finalise(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/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc deleted file mode 100644 index 0d24c9eff54ac7d86f14523ef46194c4bbc24cad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmZ9GO$q`r423JY5W#!QX3Pa-@BpGC9-x#qGe-JH(*ZrLhY(zw0apgz3w(L-vV3nh zpI3LW>NgA72NAEtoKn|jCZ|SB{TUl!a7zKfL|4!-^d;TVR)%xNc Date: Sat, 30 Mar 2013 09:46:34 +0100 Subject: [PATCH 31/71] added test --- tests/functional/openlp_core_lib/test_lib.py | 4 ++-- tests/interfaces/openlp_plugins/__init__.pyc | Bin 184 -> 172 bytes .../custom/forms/test_customform.py | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) 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/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc index 0d24c9eff54ac7d86f14523ef46194c4bbc24cad..c96ac5c5cba376cfa02ce1fb8f8faeb228d39122 100644 GIT binary patch delta 35 rcmdnNxQ3CP`7}HIi6YUhalS Date: Sat, 30 Mar 2013 10:46:32 +0100 Subject: [PATCH 32/71] fixed test order --- .../custom/forms/test_customform.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index d5de9bff9..417e52875 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -36,6 +36,20 @@ class TestCustomFrom(TestCase): del self.main_window del self.app + def load_themes_test(self): + """ + Test the load_themes() method. + """ + # GIVEN: A mocked QDialog.exec_() method + with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec: + theme_list = [u'First Theme', u'Second Theme'] + # WHEN: Show the dialog and add pass a theme list. + self.form.exec_() + 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. @@ -63,16 +77,3 @@ class TestCustomFrom(TestCase): # THEN: One slide should be added. assert self.form.slide_list_view.count() == 1, u'There should be one slide added.' - def load_themes_test(self): - """ - Test the load_themes() method. - """ - # GIVEN: A mocked QDialog.exec_() method - with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec: - theme_list = [u'First Theme', u'Second Theme'] - # WHEN: Show the dialog and add pass a theme list. - self.form.exec_() - 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.' From c732a70fb23ae83ae1a02dee8acb3141a5b31dff Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 30 Mar 2013 10:50:04 +0100 Subject: [PATCH 33/71] removed extra line --- tests/interfaces/openlp_plugins/custom/forms/test_customform.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index 417e52875..5b15688ac 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -76,4 +76,3 @@ class TestCustomFrom(TestCase): QtTest.QTest.mouseClick(self.form.add_button, QtCore.Qt.LeftButton) # THEN: One slide should be added. assert self.form.slide_list_view.count() == 1, u'There should be one slide added.' - From 76ea8126294d088d5ae4e002cd860b3f1faf7764 Mon Sep 17 00:00:00 2001 From: M2j Date: Sat, 30 Mar 2013 22:54:42 +0100 Subject: [PATCH 34/71] - switch to Python3 style sorting - use ICU for string sorting --- openlp/core/ui/exceptionform.py | 6 +++ openlp/core/ui/thememanager.py | 4 +- openlp/core/utils/__init__.py | 47 ++++++++++++++----- .../plugins/bibles/forms/bibleimportform.py | 4 +- openlp/plugins/bibles/lib/mediaitem.py | 6 +-- openlp/plugins/custom/lib/db.py | 7 ++- openlp/plugins/images/lib/mediaitem.py | 12 ++--- openlp/plugins/media/lib/mediaitem.py | 6 +-- openlp/plugins/presentations/lib/mediaitem.py | 5 +- openlp/plugins/songs/forms/songexportform.py | 4 +- openlp/plugins/songs/lib/__init__.py | 36 +------------- openlp/plugins/songs/lib/mediaitem.py | 6 +-- scripts/check_dependencies.py | 1 + 13 files changed, 69 insertions(+), 75 deletions(-) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 25f8201b1..e4854d0c0 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -69,6 +69,11 @@ try: MAKO_VERSION = mako.__version__ except ImportError: MAKO_VERSION = u'-' +try: + import icu + ICU_VERSION = u'OK' +except ImportError: + ICU_VERSION = u'-' try: import uno arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue') @@ -143,6 +148,7 @@ 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'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/thememanager.py b/openlp/core/ui/thememanager.py index 89bbe86f8..53d4a513b 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_local_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_local_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..625bd25b5 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,6 +57,7 @@ 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) @@ -379,21 +381,42 @@ def format_time(text, local_time): return re.sub('\%[a-zA-Z]', match_formatting, text) -def locale_compare(string1, string2): +def get_local_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 strings and integers. + + ``string`` + A string, list or tuple which represents the item string. + """ + if isinstance(string, basestring): + string = re.findall(r'(\d+|\D+)', string) + if len(string) == 1: + return list(get_local_key(string[0])) + elif isinstance(string, tuple): + string = list(string) + if isinstance(string, list): + for index, part in enumerate(string): + if isinstance(part, basestring): + if part.isdigit(): + string[index] = int(part) + else: + string[index] = get_local_key(part) + return string from applocation import AppLocation @@ -403,4 +426,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_local_key', u'get_natural_key'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index e360cd4a1..319989433 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_local_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_local_key) self.webTranslationComboBox.addItems(bibles) def onOsisBrowseButtonClicked(self): diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index abe3cc45a..4ae7e76b5 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_local_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_local_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_local_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..ad876b6b6 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_local_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_local_key(self.title) < get_local_key(other.title) def __eq__(self, other): - return 0 == locale_compare(self.title, other.title) + return get_local_key(self.title) == get_local_key(other.title) def init_schema(url): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index d74b1ccab..8c6bc8d9f 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -36,7 +36,7 @@ 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_local_key, get_images_filter from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups @@ -255,7 +255,7 @@ class ImageMediaItem(MediaManagerItem): 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_local_key(group_object.group_name)) folder_icon = build_icon(u':/images/image_group.png') for image_group in image_groups: group = QtGui.QTreeWidgetItem() @@ -286,7 +286,7 @@ class ImageMediaItem(MediaManagerItem): 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_local_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 + ' ') @@ -338,7 +338,7 @@ class ImageMediaItem(MediaManagerItem): self.expand_group(open_group.id) # 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_local_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] @@ -525,9 +525,9 @@ 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_local_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_local_key(item.text(0))) target_group.addChildren(image_items) def generate_slide_data(self, service_item, item=None, xmlVersion=False, diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 57bc6947b..26cb35cd2 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -37,7 +37,7 @@ 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_local_key log = logging.getLogger(__name__) @@ -261,7 +261,7 @@ 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_local_key(os.path.split(unicode(filename))[1])) for track in media: track_info = QtCore.QFileInfo(track) if not os.path.exists(track): @@ -287,7 +287,7 @@ class MediaMediaItem(MediaManagerItem): def getList(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]) + media.sort(key=lambda filename: get_local_key(os.path.split(unicode(filename))[1])) ext = [] if type == MediaType.Audio: ext = self.media_controller.audio_extensions_list diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index f92562541..a87821e8c 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -35,7 +35,7 @@ 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_local_key from openlp.plugins.presentations.lib import MessageListener log = logging.getLogger(__name__) @@ -153,8 +153,7 @@ class PresentationMediaItem(MediaManagerItem): if not initialLoad: 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_local_key(os.path.split(unicode(filename))[1])) for file in files: if not initialLoad: self.main_window.increment_progress_bar() diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 79f21a454..94748d3e4 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -37,7 +37,7 @@ 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.core.utils import get_natural_key from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport @@ -222,7 +222,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: get_natural_key(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..abeec1253 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 @@ -592,37 +592,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/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 0c4898fd9..81aab4d0f 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -38,12 +38,12 @@ from sqlalchemy.sql import or_ from openlp.core.lib import Registry, MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, Settings, \ UiStrings, translate, check_item_selected, create_separated_list, check_directory_exists from openlp.core.lib.ui import create_widget_action -from openlp.core.utils import AppLocation +from openlp.core.utils import AppLocation, get_natural_key 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: get_natural_key(song.sort_key)) for song in searchresults: # Do not display temporary songs if song.temporary: diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 4c0f69b91..a6e075db4 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -83,6 +83,7 @@ MODULES = [ 'mako', 'migrate', 'uno', + 'icu', ] From 3686da6a2eea5e1803094d70064bc0d3dd6714b6 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 31 Mar 2013 11:13:56 +0100 Subject: [PATCH 35/71] Changed regex string type to raw --- openlp/plugins/songs/lib/songshowplusimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index 14631ebc2..a72f83c4f 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -180,7 +180,7 @@ class SongShowPlusImport(SongImport): 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(u'(\D*)(\d+)', verse_name) + match = re.match(r'(\D*)(\d+)', verse_name) if match: verse_type = match.group(1).strip() verse_number = match.group(2) From 118f295204a1c8548c31919c0cce31610b382a4b Mon Sep 17 00:00:00 2001 From: M2j Date: Sun, 31 Mar 2013 12:31:54 +0200 Subject: [PATCH 36/71] Precompute the whole comparision key for song sorting. --- openlp/core/utils/__init__.py | 29 ++++++++------------ openlp/plugins/songs/forms/songexportform.py | 3 +- openlp/plugins/songs/lib/db.py | 28 +++---------------- openlp/plugins/songs/lib/mediaitem.py | 4 +-- 4 files changed, 18 insertions(+), 46 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 625bd25b5..ba7e4dd25 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -398,25 +398,18 @@ def get_local_key(string): def get_natural_key(string): """ - Generate a key for locale aware natural string sorting. Returns a list of strings and integers. - - ``string`` - A string, list or tuple which represents the item string. + Generate a key for locale aware natural string sorting. + Returns a list of string compare keys and integers. """ - if isinstance(string, basestring): - string = re.findall(r'(\d+|\D+)', string) - if len(string) == 1: - return list(get_local_key(string[0])) - elif isinstance(string, tuple): - string = list(string) - if isinstance(string, list): - for index, part in enumerate(string): - if isinstance(part, basestring): - if part.isdigit(): - string[index] = int(part) - else: - string[index] = get_local_key(part) - return string + key = re.findall(r'(\d+|\D+)', string) + if len(key) == 1: + return list(get_local_key(string)) + for index, part in enumerate(key): + if part.isdigit(): + key[index] = int(part) + else: + key[index] = get_local_key(part) + return key from applocation import AppLocation diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 94748d3e4..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.core.utils import get_natural_key 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(key=lambda song: get_natural_key(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/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 81aab4d0f..d75124d84 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -38,7 +38,7 @@ from sqlalchemy.sql import or_ from openlp.core.lib import Registry, MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, Settings, \ UiStrings, translate, check_item_selected, create_separated_list, check_directory_exists from openlp.core.lib.ui import create_widget_action -from openlp.core.utils import AppLocation, get_natural_key +from openlp.core.utils import AppLocation from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm @@ -225,7 +225,7 @@ class SongMediaItem(MediaManagerItem): log.debug(u'display results Song') self.save_auto_select_id() self.list_view.clear() - searchresults.sort(key=lambda song: get_natural_key(song.sort_key)) + searchresults.sort(key=lambda song: song.sort_key) for song in searchresults: # Do not display temporary songs if song.temporary: From 7bae770458e471c05acd94fd27de78d3b8ad151f Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 31 Mar 2013 11:47:31 +0100 Subject: [PATCH 37/71] Some test had more than one GIVEN, WHEN, THEN. Fixed that --- .../openlp_plugins/songs/test_songshowplusimport.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 24c77d0f3..236c8bcc2 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -125,7 +125,7 @@ class TestSongShowPlusImport(TestCase): def to_openlp_verse_tag_test(self): """ - Test to_openlp_verse_tag method + 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'): @@ -150,6 +150,10 @@ class TestSongShowPlusImport(TestCase): 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() From 8864e1e51b911f1ec2ecafe9dcf8c3c966a2ea6e Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Sun, 31 Mar 2013 11:55:58 +0100 Subject: [PATCH 38/71] Missed a few GIVEN, WHEN, THENS --- .../songs/test_songshowplusimport.py | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 236c8bcc2..1ae1e16f9 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -70,9 +70,9 @@ class TestSongShowPlusImport(TestCase): # THEN: The importer object should not be None self.assertIsNotNone(importer, u'Import should not be none') - def import_source_test(self): + def invalid_import_source_test(self): """ - Test SongShowPlusImport.doImport handles different import_source values + 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'): @@ -82,30 +82,19 @@ class TestSongShowPlusImport(TestCase): importer.import_wizard = mocked_import_wizard importer.stop_import_flag = True - # WHEN: Import source is a string - importer.import_source = u'not a list' + # 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') - - # 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 an int - importer.import_source = 0 - - # 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') + # 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() From 8696db9f75f018c5c344a208bad6423a371018b4 Mon Sep 17 00:00:00 2001 From: M2j Date: Sun, 31 Mar 2013 18:30:22 +0200 Subject: [PATCH 39/71] - use list compression in get_natural_key - define DIGIT_OR_NONDIGIT --- openlp/core/utils/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index ba7e4dd25..739d4ad2e 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -62,6 +62,7 @@ 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): @@ -401,14 +402,8 @@ def get_natural_key(string): Generate a key for locale aware natural string sorting. Returns a list of string compare keys and integers. """ - key = re.findall(r'(\d+|\D+)', string) - if len(key) == 1: - return list(get_local_key(string)) - for index, part in enumerate(key): - if part.isdigit(): - key[index] = int(part) - else: - key[index] = get_local_key(part) + key = DIGITS_OR_NONDIGITS.findall(string) + key = [int(part) if part.isdigit() else get_local_key(part) for part in key] return key From b3bb9bc0c0c1a5fa38b672b954188a57c6dd27f5 Mon Sep 17 00:00:00 2001 From: M2j Date: Sun, 31 Mar 2013 18:43:08 +0200 Subject: [PATCH 40/71] Python 3 transition comment --- openlp/core/utils/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 739d4ad2e..092e67913 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -404,6 +404,9 @@ def get_natural_key(string): """ key = DIGITS_OR_NONDIGITS.findall(string) key = [int(part) if part.isdigit() else get_local_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 a0eed25c6450b314a295bb4b7f167121610c9bd7 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sun, 31 Mar 2013 19:20:53 +0200 Subject: [PATCH 41/71] remove pyc --- tests/interfaces/openlp_plugins/__init__.pyc | Bin 172 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/__init__.pyc diff --git a/tests/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc deleted file mode 100644 index c96ac5c5cba376cfa02ce1fb8f8faeb228d39122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172 zcmZSn%*(~JG%PTg0SXv_v;zAHr7#>Pg*=K3Y6#XyP7ypq(Sw8Z4pV*Pxenw*08 pf}GOy%)Da#`1s7c%#!$cy@JXT4xq_4x%nxjIjMFa`-_2?0RVP?DE|Ne From e3afbefd27de25b73ef411e5b8ec2471d0f55116 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 16:42:33 +0200 Subject: [PATCH 42/71] use QDate instead of python datetime object --- openlp/core/lib/settings.py | 2 +- openlp/core/utils/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openlp/core/lib/settings.py b/openlp/core/lib/settings.py index 30a8b25d8..d55e386d6 100644 --- a/openlp/core/lib/settings.py +++ b/openlp/core/lib/settings.py @@ -126,7 +126,7 @@ class Settings(QtCore.QSettings): 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/last version test': QtCore.QDate.currentDate().addDays(-1), u'general/loop delay': 5, u'general/recent files': [], u'general/save prompt': False, diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index d32729699..efe623405 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -29,7 +29,6 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ -from datetime import datetime from distutils.version import LooseVersion import logging import locale @@ -184,7 +183,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 = QtCore.QDate.currentDate() settings.setValue(u'last version test', this_test) settings.endGroup() # Tell the main window whether there will ever be data to display From 5e3563ceda08f60380b68019d566bbe883ddc2e7 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 17:08:46 +0200 Subject: [PATCH 43/71] added two tests --- openlp/core/utils/__init__.py | 3 +- .../openlp_core_utils/test_utils.py | 49 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index efe623405..81716a9c9 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -243,8 +243,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 == 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) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 2e826bc61..71922beec 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -5,7 +5,7 @@ from unittest import TestCase from mock import patch -from openlp.core.utils import get_filesystem_encoding, _get_frozen_path +from openlp.core.utils import get_filesystem_encoding, _get_frozen_path, clean_filename, split_filename class TestUtils(TestCase): """ @@ -56,3 +56,50 @@ 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 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 (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 be valid.' From 0d621f86991f42df0474d4e39fdf5708a8bcb803 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 17:14:27 +0200 Subject: [PATCH 44/71] changed assertion message --- tests/functional/openlp_core_utils/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 71922beec..fd1aaa84e 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -102,4 +102,4 @@ class TestUtils(TestCase): result = clean_filename(invalid_name) # THEN: The file name should be cleaned. - assert result == wanted_name, u'The file name should be valid.' + assert result == wanted_name, u'The file name should not contain any special characters.' From 7f111b31a7058f13afe9ddaba826397fbab4072d Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 17:24:27 +0200 Subject: [PATCH 45/71] use unicode instead of QDateTime --- openlp/core/lib/settings.py | 3 +-- openlp/core/utils/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/lib/settings.py b/openlp/core/lib/settings.py index d55e386d6..50b1318b6 100644 --- a/openlp/core/lib/settings.py +++ b/openlp/core/lib/settings.py @@ -125,8 +125,7 @@ class Settings(QtCore.QSettings): 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': QtCore.QDate.currentDate().addDays(-1), + u'general/last version test': u'', u'general/loop delay': 5, u'general/recent files': [], u'general/save prompt': False, diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 81716a9c9..ba3e06e58 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -29,6 +29,7 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ +from datetime import datetime from distutils.version import LooseVersion import logging import locale @@ -183,7 +184,7 @@ def check_latest_version(current_version): settings = Settings() settings.beginGroup(u'general') last_test = settings.value(u'last version test') - this_test = QtCore.QDate.currentDate() + 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 From ffb2350f62f4c9ce33973b202101ab75588b3b4f Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 19:42:01 +0200 Subject: [PATCH 46/71] fix *cough* --- openlp/core/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index ba3e06e58..9cd8f8c81 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -244,7 +244,7 @@ def get_images_filter(): global IMAGES_FILTER if not IMAGES_FILTER: log.debug(u'Generating images filter.') - formats == QtGui.QImageReader.supportedImageFormats() + formats = 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) From 316902fb88c65acce89cbe143e0430419560dff1 Mon Sep 17 00:00:00 2001 From: M2j Date: Tue, 2 Apr 2013 19:51:19 +0200 Subject: [PATCH 47/71] Add unit tests for get_local_key and get_natural_key. --- .../openlp_core_utils/test_utils.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 2e826bc61..ac4da275b 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -5,7 +5,7 @@ from unittest import TestCase from mock import patch -from openlp.core.utils import get_filesystem_encoding, _get_frozen_path +from openlp.core.utils import get_filesystem_encoding, _get_frozen_path, get_local_key, get_natural_key class TestUtils(TestCase): """ @@ -56,3 +56,28 @@ 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 get_local_key_test(self): + """ + Test the get_local_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 + assert sorted(unsorted_list, key=get_local_key) == [u'Aushang', u'\u00C4u\u00DFerung', u'Auszug'], 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 + assert sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a'], u'Numbers should be sortet naturally' + From 9612a52211e32a3fda02965eb3898232d1fc2f60 Mon Sep 17 00:00:00 2001 From: M2j Date: Tue, 2 Apr 2013 19:54:23 +0200 Subject: [PATCH 48/71] typo --- tests/functional/openlp_core_utils/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index ac4da275b..1546e9a7b 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -79,5 +79,5 @@ class TestUtils(TestCase): 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 - assert sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a'], u'Numbers should be sortet naturally' + assert sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a'], u'Numbers should be sorted naturally' From 2598674d2a2b6c23a85605e888a57e1a23553005 Mon Sep 17 00:00:00 2001 From: M2j Date: Tue, 2 Apr 2013 22:52:31 +0200 Subject: [PATCH 49/71] Add ICU version string to exception form. --- openlp/core/ui/exceptionform.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index e4854d0c0..6b77a8c6f 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -71,7 +71,10 @@ except ImportError: MAKO_VERSION = u'-' try: import icu - ICU_VERSION = u'OK' + try: + ICU_VERSION = icu.VERSION + except AttributeError: + ICU_VERSION = u'OK' except ImportError: ICU_VERSION = u'-' try: From 03eaa91a08776671ea4358524ade159fd761c79f Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 23:03:07 +0200 Subject: [PATCH 50/71] fixed assertion message and method doc string --- tests/functional/openlp_core_utils/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index fd1aaa84e..6d95c6583 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -70,11 +70,11 @@ class TestUtils(TestCase): result = split_filename(file_path) # THEN: A tuple should be returned. - assert result == wanted_result, u'A tuple with the directory and file should have been 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. + Test the split_filename() function with a path to a directory """ # GIVEN: A path to a dir. file_path = u'/home/user/mydir' @@ -87,7 +87,7 @@ class TestUtils(TestCase): # THEN: A tuple should be returned. assert result == wanted_result, \ - u'A two-entry tuple with the directory and file (empty) should have been returned.' + u'A two-entry tuple with the directory and file name (empty) should have been returned.' def clean_filename_test(self): From c3583bd21dc7b684d25f69b8df2dec164ac122e3 Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Thu, 4 Apr 2013 17:42:22 +0100 Subject: [PATCH 51/71] Added comment --- openlp/plugins/songs/lib/songimport.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py index a9383c0cb..8886e2884 100644 --- a/openlp/plugins/songs/lib/songimport.py +++ b/openlp/plugins/songs/lib/songimport.py @@ -260,6 +260,8 @@ 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]) + # 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) From 42f52cc300cc380bbc891319ffc56babc421b2f9 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 5 Apr 2013 12:50:10 +0200 Subject: [PATCH 52/71] Separate blank buttons don't hide on resizing (bug #1164925) Fixes: https://launchpad.net/bugs/1164925 --- openlp/core/ui/slidecontroller.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index f0c5aa170..4893e62f4 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -588,12 +588,14 @@ 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(): + # Threshold which has to be trespassed to toggle. + threshold = 27 + # Add the threshold to prevent flickering. + if width > used_space + 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 - threshold and not self.hide_menu.isVisible(): self.toolbar.set_widget_visible(self.wide_menu, False) self.toolbar.set_widget_visible(self.narrow_menu) From 07d6fa63c5ce1c4f0f2159cb2a0d4fda0852e9c6 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 5 Apr 2013 15:41:42 +0200 Subject: [PATCH 53/71] introduced constant --- openlp/core/ui/slidecontroller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 4893e62f4..eaeebfba8 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); ' \ @@ -588,14 +590,12 @@ class SlideController(DisplayController): if self.is_live: # Space used by the toolbar. used_space = self.toolbar.size().width() + self.hide_menu.size().width() - # Threshold which has to be trespassed to toggle. - threshold = 27 # Add the threshold to prevent flickering. - if width > used_space + threshold and self.hide_menu.isVisible(): + 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) # Take away a threshold to prevent flickering. - elif width < used_space - threshold and not self.hide_menu.isVisible(): + 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) From 042bf4ed2d6567b3e4c64a0bd1ba3f9e12591217 Mon Sep 17 00:00:00 2001 From: M2j Date: Fri, 5 Apr 2013 19:41:01 +0200 Subject: [PATCH 54/71] rename openlp.core.utils.get_local_key to openlp.core.utils.get_locale_key --- openlp/core/ui/thememanager.py | 4 ++-- openlp/core/utils/__init__.py | 6 +++--- openlp/plugins/bibles/forms/bibleimportform.py | 4 ++-- openlp/plugins/bibles/lib/mediaitem.py | 6 +++--- openlp/plugins/custom/lib/db.py | 6 +++--- openlp/plugins/images/lib/mediaitem.py | 12 ++++++------ openlp/plugins/media/lib/mediaitem.py | 6 +++--- openlp/plugins/presentations/lib/mediaitem.py | 4 ++-- tests/interfaces/openlp_plugins/__init__.pyc | Bin 184 -> 166 bytes 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 53d4a513b..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, get_local_key, 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: get_local_key(unicode(file_name))) + 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 092e67913..0e48f2fa5 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -382,7 +382,7 @@ def format_time(text, local_time): return re.sub('\%[a-zA-Z]', match_formatting, text) -def get_local_key(string): +def get_locale_key(string): """ Creates a key for case insensitive, locale aware string sorting. """ @@ -403,7 +403,7 @@ def get_natural_key(string): Returns a list of string compare keys and integers. """ key = DIGITS_OR_NONDIGITS.findall(string) - key = [int(part) if part.isdigit() else get_local_key(part) for part in key] + 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 @@ -417,4 +417,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'get_local_key', u'get_natural_key'] + u'delete_file', u'clean_filename', u'format_time', u'get_locale_key', u'get_natural_key'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 319989433..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, get_local_key +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(key=get_local_key) + bibles.sort(key=get_locale_key) self.webTranslationComboBox.addItems(bibles) def onOsisBrowseButtonClicked(self): diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 4ae7e76b5..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 get_local_key +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(key=get_local_key) + 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(key=get_local_key) + 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 ad876b6b6..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 get_local_key +from openlp.core.utils import get_locale_key class CustomSlide(BaseModel): """ @@ -44,10 +44,10 @@ class CustomSlide(BaseModel): # By default sort the customs by its title considering language specific # characters. def __lt__(self, other): - return get_local_key(self.title) < get_local_key(other.title) + return get_locale_key(self.title) < get_locale_key(other.title) def __eq__(self, other): - return get_local_key(self.title) == get_local_key(other.title) + return get_locale_key(self.title) == get_locale_key(other.title) def init_schema(url): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 8c6bc8d9f..fc575ec0a 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -36,7 +36,7 @@ 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, get_local_key, 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 @@ -255,7 +255,7 @@ class ImageMediaItem(MediaManagerItem): 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(key=lambda group_object: get_local_key(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() @@ -286,7 +286,7 @@ class ImageMediaItem(MediaManagerItem): 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(key=lambda group_object: get_local_key(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 + ' ') @@ -338,7 +338,7 @@ class ImageMediaItem(MediaManagerItem): self.expand_group(open_group.id) # Sort the images by its filename considering language specific # characters. - images.sort(key=lambda image_object: get_local_key(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] @@ -525,9 +525,9 @@ class ImageMediaItem(MediaManagerItem): group_items.append(item) if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames): image_items.append(item) - group_items.sort(key=lambda item: get_local_key(item.text(0))) + group_items.sort(key=lambda item: get_locale_key(item.text(0))) target_group.addChildren(group_items) - image_items.sort(key=lambda item: get_local_key(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, diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 26cb35cd2..7d492bc69 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -37,7 +37,7 @@ 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, get_local_key +from openlp.core.utils import AppLocation, get_locale_key log = logging.getLogger(__name__) @@ -261,7 +261,7 @@ class MediaMediaItem(MediaManagerItem): def load_list(self, media, target_group=None): # Sort the media by its filename considering language specific # characters. - media.sort(key=lambda filename: get_local_key(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): @@ -287,7 +287,7 @@ class MediaMediaItem(MediaManagerItem): def getList(self, type=MediaType.Audio): media = Settings().value(self.settings_section + u'/media files') - media.sort(key=lambda filename: get_local_key(os.path.split(unicode(filename))[1])) + media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1])) ext = [] if type == MediaType.Audio: ext = self.media_controller.audio_extensions_list diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index a87821e8c..52dcd891f 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -35,7 +35,7 @@ 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 get_local_key +from openlp.core.utils import get_locale_key from openlp.plugins.presentations.lib import MessageListener log = logging.getLogger(__name__) @@ -153,7 +153,7 @@ class PresentationMediaItem(MediaManagerItem): if not initialLoad: self.main_window.display_progress_bar(len(files)) # Sort the presentations by its filename considering language specific characters. - files.sort(key=lambda filename: get_local_key(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: self.main_window.increment_progress_bar() diff --git a/tests/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc index 0d24c9eff54ac7d86f14523ef46194c4bbc24cad..41b2c438107d1adf4a5d502219772f510965f920 100644 GIT binary patch delta 58 zcmdnNxQvmV`7~wiuL^qQuBNQ e^oxKJ`MJ79sp& Date: Fri, 5 Apr 2013 19:26:39 +0100 Subject: [PATCH 55/71] Removed the use of the mock's call helper object --- .../openlp_plugins/songs/test_songshowplusimport.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py index 1ae1e16f9..86d77bbdc 100644 --- a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py @@ -4,7 +4,7 @@ This module contains tests for the SongShow Plus song importer. import os from unittest import TestCase -from mock import call, patch, MagicMock +from mock import patch, MagicMock from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.songshowplusimport import SongShowPlusImport @@ -195,11 +195,10 @@ class TestSongShowPlusImport(TestCase): # WHEN: Importing each file importer.import_source = [os.path.join(TEST_PATH, song_file)] title = SONG_TEST_DATA[song_file][u'title'] - parse_author_calls = [call(author) for author in SONG_TEST_DATA[song_file][u'authors']] + 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 = \ - [call(verse_text, verse_tag) for verse_text, verse_tag in SONG_TEST_DATA[song_file][u'verses']] + 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'] @@ -210,13 +209,15 @@ class TestSongShowPlusImport(TestCase): # 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)) - mocked_parse_author.assert_has_calls(parse_author_calls) + 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)) - mocked_add_verse.assert_has_calls(add_verse_calls) + 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: From fc80e3d106dacb89621e323e00cb1f9f976266df Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 5 Apr 2013 20:37:15 +0100 Subject: [PATCH 56/71] Cleanups and more tests --- openlp/core/lib/plugin.py | 4 +- openlp/core/lib/pluginmanager.py | 2 +- openlp/plugins/media/mediaplugin.py | 2 +- .../presentations/presentationplugin.py | 2 +- .../openlp_core_lib/test_pluginmanager.py | 12 +- .../openlp_core_utils/test_applocation.py | 10 ++ .../openlp_plugins/remotes/test_remotetab.py | 108 ++++++++++++++++++ tests/resources/remotes/openlp.crt | 0 tests/resources/remotes/openlp.key | 0 9 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 tests/functional/openlp_plugins/remotes/test_remotetab.py create mode 100644 tests/resources/remotes/openlp.crt create mode 100644 tests/resources/remotes/openlp.key 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/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 3e685f4c6..4bc5314ff 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -54,7 +54,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 """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 7872c25b7..5bc95e388 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -69,7 +69,7 @@ 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 """ 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_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_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/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 From 6d5590d7396111d96bc6359a688fbb7fc5239121 Mon Sep 17 00:00:00 2001 From: M2j Date: Sat, 6 Apr 2013 19:17:59 +0200 Subject: [PATCH 57/71] removed tests/interfaces/openlp_plugins/__init__.pyc --- tests/interfaces/openlp_plugins/__init__.pyc | Bin 166 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/__init__.pyc diff --git a/tests/interfaces/openlp_plugins/__init__.pyc b/tests/interfaces/openlp_plugins/__init__.pyc deleted file mode 100644 index 41b2c438107d1adf4a5d502219772f510965f920..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166 zcmZSn%*(|R7ao|*00oRd+5w1*S%5?e14FO|NW@PANHCxg#r{As{fzwFRQ=r4%)Hd1 z68*puegA^gJf8slq|$U Date: Sat, 6 Apr 2013 20:21:23 +0200 Subject: [PATCH 58/71] renameing openlp.core.utils.get_local_key in tests --- tests/functional/openlp_core_utils/test_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 1546e9a7b..b9decb37e 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -5,7 +5,7 @@ from unittest import TestCase from mock import patch -from openlp.core.utils import get_filesystem_encoding, _get_frozen_path, get_local_key, get_natural_key +from openlp.core.utils import get_filesystem_encoding, _get_frozen_path, get_locale_key, get_natural_key class TestUtils(TestCase): """ @@ -56,9 +56,9 @@ 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 get_local_key_test(self): + def get_locale_key_test(self): """ - Test the get_local_key(string) function + 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 @@ -67,7 +67,8 @@ class TestUtils(TestCase): 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 - assert sorted(unsorted_list, key=get_local_key) == [u'Aushang', u'\u00C4u\u00DFerung', u'Auszug'], u'Strings should be sorted properly' + 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): """ @@ -79,5 +80,6 @@ class TestUtils(TestCase): 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 - assert sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a'], u'Numbers should be sorted naturally' + 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' From 31b4e561b4ef54e40b795ce0e4274f22994eb11e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 8 Apr 2013 17:53:11 +0100 Subject: [PATCH 59/71] Fix up code review change --- openlp/core/lib/serviceitem.py | 6 ++---- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- tests/functional/openlp_plugins/remotes/test_router.py | 8 ++++---- tests/interfaces/openlp_plugins/remotes/test_server.py | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) 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/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 0489428ac..083f36db0 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -132,7 +132,7 @@ from cherrypy._cpcompat import sha, ntob log = logging.getLogger(__name__) -def sha_password_encrypter(password): +def make_sha_hash(password): """ Create an encrypted password for the given password. """ @@ -145,7 +145,7 @@ def fetch_password(username): """ if username != Settings().value(u'remotes/user id'): return None - return sha_password_encrypter(Settings().value(u'remotes/password')) + return make_sha_hash(Settings().value(u'remotes/password')) class HttpServer(object): @@ -207,7 +207,7 @@ class HttpServer(object): 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': sha_password_encrypter}, + 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}, diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py index 3b344c3b2..2980a339b 100644 --- a/tests/functional/openlp_plugins/remotes/test_router.py +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -8,7 +8,7 @@ from tempfile import mkstemp from mock import MagicMock from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, sha_password_encrypter +from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash from PyQt4 import QtGui __default_settings__ = { @@ -62,7 +62,7 @@ class TestRouter(TestCase): # GIVEN: A default configuration # WHEN: called with the defined userid password = fetch_password(u'openlp') - required_password = sha_password_encrypter(u'password') + 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') @@ -73,12 +73,12 @@ class TestRouter(TestCase): """ # GIVEN: A default configuration # WHEN: called with the defined userid - required_password = sha_password_encrypter(u'password') + 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 sha_password_encrypter should return the correct encrypted password') + u'The result for make_sha_hash should return the correct encrypted password') def process_http_request_test(self): """ diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py index d7ce88010..9ae544d21 100644 --- a/tests/interfaces/openlp_plugins/remotes/test_server.py +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -9,10 +9,10 @@ from mock import MagicMock import urllib2 import cherrypy -from BeautifulSoup import BeautifulSoup, NavigableString, Tag +from BeautifulSoup import BeautifulSoup from openlp.core.lib import Settings -from openlp.plugins.remotes.lib.httpserver import HttpServer, fetch_password, sha_password_encrypter +from openlp.plugins.remotes.lib.httpserver import HttpServer from PyQt4 import QtGui __default_settings__ = { From d504b314ef4f316d510cf162ffacd2d3af68d16d Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 8 Apr 2013 20:30:11 +0200 Subject: [PATCH 60/71] removed not needed mocks and calls --- .../custom/forms/test_customform.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index d6e69946f..95b7d7ffa 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -40,29 +40,25 @@ class TestEditCustomForm(TestCase): """ Test the load_themes() method. """ - # GIVEN: A mocked QDialog.exec_() method - with patch(u'PyQt4.QtGui.QDialog.exec_') as mocked_exec: - theme_list = [u'First Theme', u'Second Theme'] - # WHEN: Show the dialog and add pass a theme list. - self.form.exec_() - self.form.load_themes(theme_list) + # GIVEN: A theme list. + theme_list = [u'First Theme', u'Second Theme'] - # 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.' + # 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): @@ -71,8 +67,8 @@ 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. assert self.form.slide_list_view.count() == 1, u'There should be one slide added.' From 220681c900981c0aad6c7b42e476aba227ea38e4 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 14 Apr 2013 16:59:57 +0100 Subject: [PATCH 61/71] Comment fixes --- openlp/core/lib/mediamanageritem.py | 6 ++++++ openlp/plugins/remotes/lib/httpserver.py | 6 ++++++ .../openlp_plugins/remotes/test_server.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index d27ef4041..01329b842 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -487,6 +487,9 @@ class MediaManagerItem(QtGui.QWidget): def go_live_remote(self, message): """ Remote Call wrapper + + ``message`` + The passed data item_id:Remote. """ self.go_live(message[0], remote=message[1]) @@ -535,6 +538,9 @@ class MediaManagerItem(QtGui.QWidget): 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]) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 083f36db0..203b67205 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -285,6 +285,12 @@ class HttpRouter(object): 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: diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py index 9ae544d21..8795eeaf3 100644 --- a/tests/interfaces/openlp_plugins/remotes/test_server.py +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -101,6 +101,15 @@ class TestRouter(TestCase): 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) @@ -115,6 +124,15 @@ def call_remote_server(url, username=None, password=None): 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 From db00c5049a012912e47fd1457009fcd1403311f3 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 17:50:59 +0200 Subject: [PATCH 62/71] fixed missing unicode --- openlp/core/utils/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 9a03c2b0e..d4ee8039c 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -247,7 +247,7 @@ def get_images_filter(): global IMAGES_FILTER if not IMAGES_FILTER: log.debug(u'Generating images filter.') - formats = 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) @@ -405,7 +405,7 @@ def get_natural_key(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 return key From 7f78250bb2798cea89349febf332c5fcf1c1edfb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 15 Apr 2013 18:55:50 +0100 Subject: [PATCH 63/71] Make object --- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 203b67205..878b197b3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -216,7 +216,7 @@ class HttpServer(object): u'tools.basic_auth.on': False}} return directory_config - class Public: + class Public(object): """ Main access class with may have security enabled on it. """ @@ -228,7 +228,7 @@ class HttpServer(object): url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) - class Files: + class Files(object): """ Provides access to files and has no security available. These are read only accesses """ @@ -237,7 +237,7 @@ class HttpServer(object): url = urlparse.urlparse(cherrypy.url()) return self.router.process_http_request(url.path, *args) - class Stage: + class Stage(object): """ Stageview is read only so security is not relevant and would reduce it's usability """ From dad243dd743835acb79c80e2f5a753406b916a2e Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 20:52:39 +0200 Subject: [PATCH 64/71] songusage and presentation code standards --- openlp/plugins/presentations/__init__.py | 4 +- .../presentations/lib/impresscontroller.py | 127 ++++++++---------- openlp/plugins/presentations/lib/mediaitem.py | 117 ++++++++-------- .../presentations/lib/messagelistener.py | 67 +++++---- .../presentations/lib/powerpointcontroller.py | 55 ++++---- .../presentations/lib/pptviewcontroller.py | 96 +++++++------ .../lib/presentationcontroller.py | 84 +++++------- .../presentations/lib/presentationtab.py | 7 +- .../presentations/presentationplugin.py | 50 ++++--- openlp/plugins/songusage/__init__.py | 5 +- .../songusage/forms/songusagedeletedialog.py | 3 +- .../songusage/forms/songusagedetaildialog.py | 3 +- openlp/plugins/songusage/lib/db.py | 2 + openlp/plugins/songusage/songusageplugin.py | 20 ++- 14 files changed, 301 insertions(+), 339 deletions(-) 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..e5df6685e 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 = self.controller.manager.Bridge_GetStruct(u'com.sun.star.beans.PropertyValue') else: - prop = PropertyValue() - prop.Name = name - prop.Value = value - return prop + property = PropertyValue() + property.Name = name + property.Value = value + return property 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 52dcd891f..9664fcf2f 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -38,14 +38,17 @@ from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adj 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, + 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.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.presentation_widget, u'displayTypeComboBox') - self.displayTypeLabel.setBuddy(self.displayTypeComboBox) - self.displayLayout.addRow(self.displayTypeLabel, self.displayTypeComboBox) + self.display_type_label.setBuddy(self.displayTypeComboBox) + self.display_layout.addRow(self.display_type_label, self.displayTypeComboBox) # Add the Presentation widget to the page layout - self.page_layout.addWidget(self.presentationWidget) + self.page_layout.addWidget(self.presentation_widget) def initialise(self): """ @@ -120,13 +123,13 @@ 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() for item in self.controllers: @@ -137,38 +140,37 @@ class PresentationMediaItem(MediaManagerItem): self.displayTypeComboBox.insertItem(0, self.Automatic) self.displayTypeComboBox.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(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.') @@ -180,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() @@ -192,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, @@ -203,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() @@ -230,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] @@ -287,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 5bc95e388..08f16fa12 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,22 +39,23 @@ 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, - u'presentations/Powerpoint': QtCore.Qt.Checked, - u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, - u'presentations/presentations files': [] - } + u'presentations/override app': QtCore.Qt.Unchecked, + u'presentations/Impress': QtCore.Qt.Checked, + u'presentations/Powerpoint': QtCore.Qt.Checked, + u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, + u'presentations/presentations files': [] +} 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') @@ -71,16 +72,14 @@ class PresentationPlugin(Plugin): 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/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): From b03c3c6f4fc4701dc693147947f2cf6492b9bcff Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 21:03:16 +0200 Subject: [PATCH 65/71] code standards --- openlp/plugins/images/__init__.py | 4 +- openlp/plugins/images/forms/__init__.py | 25 ++++---- openlp/plugins/images/forms/addgroupform.py | 10 ++-- .../plugins/images/forms/choosegroupform.py | 8 +-- openlp/plugins/images/imageplugin.py | 10 ++-- openlp/plugins/images/lib/db.py | 6 +- openlp/plugins/images/lib/mediaitem.py | 59 +++++++++---------- openlp/plugins/media/lib/mediaitem.py | 3 + 8 files changed, 60 insertions(+), 65 deletions(-) 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..f891969c6 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 fc575ec0a..5281070fb 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -40,6 +40,7 @@ from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_imag from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups + log = logging.getLogger(__name__) @@ -60,12 +61,11 @@ 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) @@ -77,7 +77,7 @@ class ImageMediaItem(MediaManagerItem): 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 @@ -99,8 +99,8 @@ class ImageMediaItem(MediaManagerItem): 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) @@ -159,21 +159,18 @@ class ImageMediaItem(MediaManagerItem): 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.onAddGroupClick) 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) @@ -189,10 +186,10 @@ class ImageMediaItem(MediaManagerItem): 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,13 +243,13 @@ 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(key=lambda group_object: get_locale_key(group_object.group_name)) @@ -271,16 +268,16 @@ 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() @@ -293,13 +290,13 @@ class ImageMediaItem(MediaManagerItem): 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: diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 7d492bc69..751d7a202 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -39,14 +39,17 @@ 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, 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')) + class MediaMediaItem(MediaManagerItem): """ This is the custom media manager item for Media Slides. From 582f1aa003b4c1efde5732d3c72bd10ef1ceb8eb Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 21:07:25 +0200 Subject: [PATCH 66/71] code standards --- openlp/plugins/images/lib/mediaitem.py | 45 +++++++++---------- .../openlp_plugins/images/test_lib.py | 30 ++++++------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 5281070fb..716db6c8a 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -94,7 +94,7 @@ 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): @@ -163,7 +163,7 @@ class ImageMediaItem(MediaManagerItem): """ 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) @@ -173,16 +173,16 @@ class ImageMediaItem(MediaManagerItem): 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) + icon=u':/slides/slide_blank.png', triggers=self.on_replace_click) self.resetAction = self.toolbar.add_toolbar_action(u'resetAction', - icon=u':/system/system_close.png', visible=False, triggers=self.onResetClick) + icon=u':/system/system_close.png', visible=False, triggers=self.on_reset_click) def recursively_delete_group(self, image_group): """ @@ -311,29 +311,29 @@ 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(key=lambda image_object: get_locale_key(os.path.split(unicode(image_object.filename))[1])) for imageFile in images: @@ -452,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() @@ -479,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): """ @@ -527,7 +527,7 @@ class ImageMediaItem(MediaManagerItem): 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. @@ -605,7 +605,7 @@ class ImageMediaItem(MediaManagerItem): else: return False - def onAddGroupClick(self): + def on_add_group_click(self): """ Called to add a new group """ @@ -626,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) @@ -635,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.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) - def onReplaceClick(self): + def on_replace_click(self): """ Called to replace Live backgound with the image selected. """ @@ -660,7 +659,7 @@ 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): 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' From 7682698cf1b6742e29137903466a407eabe47f9b Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 21:15:12 +0200 Subject: [PATCH 67/71] media code standards --- openlp/plugins/images/lib/mediaitem.py | 20 ++-- openlp/plugins/media/__init__.py | 7 +- openlp/plugins/media/lib/mediaitem.py | 110 +++++++++--------- openlp/plugins/media/mediaplugin.py | 14 ++- openlp/plugins/presentations/lib/mediaitem.py | 24 ++-- openlp/plugins/songs/forms/editsongform.py | 2 +- 6 files changed, 88 insertions(+), 89 deletions(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 716db6c8a..95f0971fd 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -70,10 +70,10 @@ class ImageMediaItem(MediaManagerItem): 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): """ @@ -155,7 +155,7 @@ 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): """ @@ -179,9 +179,9 @@ class ImageMediaItem(MediaManagerItem): """ Add custom buttons to the end of the toolbar """ - self.replaceAction = self.toolbar.add_toolbar_action(u'replaceAction', + self.replace_action = self.toolbar.add_toolbar_action(u'replace_action', icon=u':/slides/slide_blank.png', triggers=self.on_replace_click) - self.resetAction = self.toolbar.add_toolbar_action(u'resetAction', + 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): @@ -641,14 +641,14 @@ class ImageMediaItem(MediaManagerItem): """ 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. """ - self.resetAction.setVisible(False) + self.reset_action.setVisible(False) def on_replace_click(self): """ @@ -664,7 +664,7 @@ class ImageMediaItem(MediaManagerItem): 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 751d7a202..2037346ad 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -44,10 +44,10 @@ 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): @@ -82,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): @@ -101,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] @@ -135,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): """ @@ -158,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.')) @@ -167,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. @@ -184,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) @@ -212,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 (*)') % ( @@ -225,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): """ @@ -270,34 +268,34 @@ class MediaMediaItem(MediaManagerItem): 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(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1])) - ext = [] + 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 0bd95a26b..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': [] } @@ -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/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 9664fcf2f..2f48b99c1 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -110,11 +110,11 @@ class PresentationMediaItem(MediaManagerItem): 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.displayTypeComboBox = create_horizontal_adjusting_combo_box(self.presentation_widget, - u'displayTypeComboBox') - self.display_type_label.setBuddy(self.displayTypeComboBox) - self.display_layout.addRow(self.display_type_label, self.displayTypeComboBox) - # Add the Presentation widget to the page layout + 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): @@ -131,14 +131,14 @@ class PresentationMediaItem(MediaManagerItem): 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.presentation_widget.show() else: @@ -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 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): From a2fddbb57c65c42280bd67d9fa3c4947da287536 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 21:43:17 +0200 Subject: [PATCH 68/71] fixed fullstop --- openlp/plugins/images/forms/addgroupform.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/images/forms/addgroupform.py b/openlp/plugins/images/forms/addgroupform.py index f891969c6..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() From 465fc7bff17f6f4cd1dcaa5d853f0588f6c71c27 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 20 Apr 2013 11:02:45 +0200 Subject: [PATCH 69/71] do not overrie build-ins --- openlp/plugins/presentations/lib/impresscontroller.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index e5df6685e..d30c71078 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -295,12 +295,12 @@ class ImpressDocument(PresentationDocument): """ log.debug(u'create property OpenOffice') if os.name == u'nt': - property = 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: - property = PropertyValue() - property.Name = name - property.Value = value - return property + property_object = PropertyValue() + property_object.Name = name + property_object.Value = value + return property_object def close_presentation(self): """ From f82000eaa25580b13181ded45170e90ea487f28b Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 20 Apr 2013 11:06:26 +0200 Subject: [PATCH 70/71] reverted change to avoid conflict --- openlp/plugins/presentations/presentationplugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 08f16fa12..1cb966aa5 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -44,12 +44,12 @@ log = logging.getLogger(__name__) __default_settings__ = { - u'presentations/override app': QtCore.Qt.Unchecked, - u'presentations/Impress': QtCore.Qt.Checked, - u'presentations/Powerpoint': QtCore.Qt.Checked, - u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, - u'presentations/presentations files': [] -} + u'presentations/override app': QtCore.Qt.Unchecked, + u'presentations/Impress': QtCore.Qt.Checked, + u'presentations/Powerpoint': QtCore.Qt.Checked, + u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, + u'presentations/presentations files': [] + } class PresentationPlugin(Plugin): From 401d4d268875b78858114e2c9165149ad15a29f4 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 20 Apr 2013 20:39:10 +0200 Subject: [PATCH 71/71] close file --- tests/functional/openlp_core_lib/test_serviceitem.py | 2 ++ 1 file changed, 2 insertions(+) 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