From 2dfb7bec9cb5bfc1e69c8fb24fb3ac387592b187 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 5 Jan 2013 18:08:01 +0100 Subject: [PATCH 001/235] Added basic duplicate song detection, no removal or fancy GUI yet. --- openlp/plugins/songs/lib/doublesfinder.py | 114 ++++++++++++++++++++++ openlp/plugins/songs/songsplugin.py | 32 +++++- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 openlp/plugins/songs/lib/doublesfinder.py diff --git a/openlp/plugins/songs/lib/doublesfinder.py b/openlp/plugins/songs/lib/doublesfinder.py new file mode 100644 index 000000000..f1098a7c3 --- /dev/null +++ b/openlp/plugins/songs/lib/doublesfinder.py @@ -0,0 +1,114 @@ +# -*- 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:`dreambeamimport` module provides the functionality for importing +DreamBeam songs into the OpenLP database. +""" +import logging +import difflib + +from openlp.core.lib import translate +from openlp.plugins.songs.lib.db import Song +from openlp.plugins.songs.lib.ui import SongStrings + +log = logging.getLogger(__name__) + +class DuplicateSongFinder(object): + """ + The :class:`DreamBeamImport` class provides functionality to search for + and remove duplicate songs. + """ + + def __init__(self): + self.minFragmentSize = 5 + self.minBlockSize = 70 + self.maxTypoSize = 3 + + def songsProbablyEqual(self, song1, song2): + if len(song1.search_lyrics) < len(song2.search_lyrics): + small = song1.search_lyrics + large = song2.search_lyrics + else: + small = song2.search_lyrics + large = song1.search_lyrics + differ = difflib.SequenceMatcher(a=small, b=large) + diff_tuples = differ.get_opcodes() + diff_no_typos = self.__removeTypos(diff_tuples) + #print(diff_no_typos) + if self.__lengthOfEqualBlocks(diff_no_typos) >= self.minBlockSize or \ + self.__lengthOfLongestEqualBlock(diff_no_typos) > len(small)*2/3: + return True + else: + return False + + def __opLength(self, opcode): + return max(opcode[2]-opcode[1], opcode[4] - opcode[3]) + + def __removeTypos(self, diff): + #remove typo at beginning of string + if len(diff) >= 2: + if diff[0][0] != "equal" and self.__opLength(diff[0]) <= self.maxTypoSize and \ + self.__opLength(diff[1]) >= self.minFragmentSize: + del diff[0] + #remove typos in the middle of string + if len(diff) >= 3: + for index in range(len(diff)-3, -1, -1): + if self.__opLength(diff[index]) >= self.minFragmentSize and \ + diff[index+1][0] != "equal" and self.__opLength(diff[index+1]) <= self.maxTypoSize and \ + self.__opLength(diff[index+2]) >= self.minFragmentSize: + del diff[index+1] + #remove typo at the end of string + if len(diff) >= 2: + if self.__opLength(diff[-2]) >= self.minFragmentSize and \ + diff[-1][0] != "equal" and self.__opLength(diff[-1]) <= self.maxTypoSize: + del diff[-1] + + #merge fragments + for index in range(len(diff)-2, -1, -1): + if diff[index][0] == "equal" and self.__opLength(diff[index]) >= self.minFragmentSize and \ + diff[index+1][0] == "equal" and self.__opLength(diff[index+1]) >= self.minFragmentSize: + diff[index] = ("equal", diff[index][1], diff[index+1][2], diff[index][3], + diff[index+1][4]) + del diff[index+1] + + return diff + + def __lengthOfEqualBlocks(self, diff): + length = 0 + for element in diff: + if element[0] == "equal" and self.__opLength(element) >= self.minBlockSize: + length += self.__opLength(element) + return length + + def __lengthOfLongestEqualBlock(self, diff): + length = 0 + for element in diff: + if element[0] == "equal" and self.__opLength(element) > length: + length = self.__opLength(element) + return length diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 83e54512c..9c696489d 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -45,6 +45,7 @@ from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \ from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport +from openlp.plugins.songs.lib.doublesfinder import DuplicateSongFinder log = logging.getLogger(__name__) @@ -77,10 +78,12 @@ class SongsPlugin(Plugin): self.songImportItem.setVisible(True) self.songExportItem.setVisible(True) self.toolsReindexItem.setVisible(True) + self.toolsFindDuplicates.setVisible(True) action_list = ActionList.get_instance() action_list.add_action(self.songImportItem, UiStrings().Import) action_list.add_action(self.songExportItem, UiStrings().Export) action_list.add_action(self.toolsReindexItem, UiStrings().Tools) + action_list.add_action(self.toolsFindDuplicates, UiStrings().Tools) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'servicemanager_new_service'), self.clearTemporarySongs) @@ -122,7 +125,7 @@ class SongsPlugin(Plugin): def addToolsMenuItem(self, tools_menu): """ - Give the alerts plugin the opportunity to add items to the + Give the Songs plugin the opportunity to add items to the **Tools** menu. ``tools_menu`` @@ -137,6 +140,12 @@ class SongsPlugin(Plugin): 'Re-index the songs database to improve searching and ordering.'), visible=False, triggers=self.onToolsReindexItemTriggered) tools_menu.addAction(self.toolsReindexItem) + self.toolsFindDuplicates = create_action(tools_menu, u'toolsFindDuplicates', + text=translate('SongsPlugin', 'Find &duplicate songs'), + statustip=translate('SongsPlugin', + 'Find and remove duplicate songs in the song database.'), + visible=False, triggers=self.onToolsFindDuplicatesTriggered) + tools_menu.addAction(self.toolsFindDuplicates) def onToolsReindexItemTriggered(self): """ @@ -157,6 +166,25 @@ class SongsPlugin(Plugin): self.manager.save_objects(songs) self.mediaItem.onSearchTextButtonClicked() + def onToolsFindDuplicatesTriggered(self): + """ + Search for duplicates in the song database. + """ + maxSongs = self.manager.get_object_count(Song) + if maxSongs == 0: + return + QtGui.QMessageBox.information(self.formParent, + "Find duplicates called", "Called...") + songs = self.manager.get_all_objects(Song) + for outerSongCounter in range(maxSongs-1): + for innerSongCounter in range(outerSongCounter+1, maxSongs): + doubleFinder = DuplicateSongFinder() + if doubleFinder.songsProbablyEqual(songs[outerSongCounter], + songs[innerSongCounter]): + QtGui.QMessageBox.information(self.formParent, + "Double found", str(innerSongCounter) + " " + + str(outerSongCounter)) + def onSongImportItemClicked(self): if self.mediaItem: self.mediaItem.onImportClick() @@ -280,10 +308,12 @@ class SongsPlugin(Plugin): self.songImportItem.setVisible(False) self.songExportItem.setVisible(False) self.toolsReindexItem.setVisible(False) + self.toolsFindDuplicates.setVisible(False) action_list = ActionList.get_instance() action_list.remove_action(self.songImportItem, UiStrings().Import) action_list.remove_action(self.songExportItem, UiStrings().Export) action_list.remove_action(self.toolsReindexItem, UiStrings().Tools) + action_list.remove_action(self.toolsFindDuplicates, UiStrings().Tools) Plugin.finalise(self) def clearTemporarySongs(self): From d0e83c1c7c031cfeb70cd810a42bf5fdea41ab36 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sun, 6 Jan 2013 23:28:29 +0100 Subject: [PATCH 002/235] Commit missing file. --- .../songs/lib/{doublesfinder.py => duplicatesongfinder.py} | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) rename openlp/plugins/songs/lib/{doublesfinder.py => duplicatesongfinder.py} (96%) diff --git a/openlp/plugins/songs/lib/doublesfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py similarity index 96% rename from openlp/plugins/songs/lib/doublesfinder.py rename to openlp/plugins/songs/lib/duplicatesongfinder.py index f1098a7c3..6ace0a68b 100644 --- a/openlp/plugins/songs/lib/doublesfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -26,10 +26,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -""" -The :mod:`dreambeamimport` module provides the functionality for importing -DreamBeam songs into the OpenLP database. -""" import logging import difflib @@ -41,7 +37,7 @@ log = logging.getLogger(__name__) class DuplicateSongFinder(object): """ - The :class:`DreamBeamImport` class provides functionality to search for + The :class:`DuplicateSongFinder` class provides functionality to search for and remove duplicate songs. """ From a4ce06fa9b4ae4fcc777c676777b07878f21ddee Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sun, 6 Jan 2013 23:35:00 +0100 Subject: [PATCH 003/235] Fix last commit, I actually renamed a file and did not adapt the code to reflect this. --- openlp/plugins/songs/songsplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 9c696489d..726cbfbf5 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -45,7 +45,7 @@ from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \ from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport -from openlp.plugins.songs.lib.doublesfinder import DuplicateSongFinder +from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder log = logging.getLogger(__name__) From d85ede45b82bc222dfe9a51f085fc18f34c8f152 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 8 Jan 2013 21:06:57 +0100 Subject: [PATCH 004/235] Added a wizard. Non functional yet. Changed OpenLPWizard to allow for no progressPage to be added. --- openlp/core/ui/wizard.py | 15 +- .../songs/forms/duplicatesongremovalform.py | 139 ++++++++++++++++++ openlp/plugins/songs/songsplugin.py | 6 + 3 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 openlp/plugins/songs/forms/duplicatesongremovalform.py diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 3609302b5..be71a0bbe 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -79,9 +79,10 @@ class OpenLPWizard(QtGui.QWizard): Generic OpenLP wizard to provide generic functionality and a unified look and feel. """ - def __init__(self, parent, plugin, name, image): + def __init__(self, parent, plugin, name, image, addProgressPage=True): QtGui.QWizard.__init__(self, parent) self.plugin = plugin + self.withProgressPage = addProgressPage self.setObjectName(name) self.openIcon = build_icon(u':/general/general_open.png') self.deleteIcon = build_icon(u':/general/general_delete.png') @@ -92,8 +93,9 @@ class OpenLPWizard(QtGui.QWizard): self.customInit() self.customSignals() QtCore.QObject.connect(self, QtCore.SIGNAL(u'currentIdChanged(int)'), self.onCurrentIdChanged) - QtCore.QObject.connect(self.errorCopyToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorCopyToButtonClicked) - QtCore.QObject.connect(self.errorSaveToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorSaveToButtonClicked) + if self.withProgressPage: + QtCore.QObject.connect(self.errorCopyToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorCopyToButtonClicked) + QtCore.QObject.connect(self.errorSaveToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorSaveToButtonClicked) def setupUi(self, image): """ @@ -106,7 +108,8 @@ class OpenLPWizard(QtGui.QWizard): QtGui.QWizard.NoBackButtonOnLastPage) add_welcome_page(self, image) self.addCustomPages() - self.addProgressPage() + if self.withProgressPage: + self.addProgressPage() self.retranslateUi() def registerFields(self): @@ -168,7 +171,7 @@ class OpenLPWizard(QtGui.QWizard): Stop the wizard on cancel button, close button or ESC key. """ log.debug(u'Wizard cancelled by user.') - if self.currentPage() == self.progressPage: + if self.withProgressPage and self.currentPage() == self.progressPage: Receiver.send_message(u'openlp_stop_wizard') self.done(QtGui.QDialog.Rejected) @@ -176,7 +179,7 @@ class OpenLPWizard(QtGui.QWizard): """ Perform necessary functions depending on which wizard page is active. """ - if self.page(pageId) == self.progressPage: + if self.withProgressPage and self.page(pageId) == self.progressPage: self.preWizard() self.performWizard() self.postWizard() diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py new file mode 100644 index 000000000..33cd64b4a --- /dev/null +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 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 duplicate song removal logic for OpenLP. +""" +import codecs +import logging +import os + +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import Receiver, Settings, SettingsManager, translate +from openlp.core.lib.ui import UiStrings, critical_error_message_box +from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect + +log = logging.getLogger(__name__) + +class DuplicateSongRemovalForm(OpenLPWizard): + """ + This is the Duplicate Song Removal Wizard. It provides functionality to + search for and remove duplicate songs in the database. + """ + log.info(u'DuplicateSongRemovalForm loaded') + + def __init__(self, parent, plugin): + """ + Instantiate the wizard, and run any extra setup we need to. + + ``parent`` + The QWidget-derived parent of the wizard. + + ``plugin`` + The songs plugin. + """ + self.clipboard = plugin.formParent.clipboard + OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', + u':/wizards/wizard_importsong.bmp', False) + + def customInit(self): + """ + Song wizard specific initialisation. + """ + pass + + def customSignals(self): + """ + Song wizard specific signals. + """ + #QtCore.QObject.connect(self.addButton, + # QtCore.SIGNAL(u'clicked()'), self.onAddButtonClicked) + #QtCore.QObject.connect(self.removeButton, + # QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) + + def addCustomPages(self): + """ + Add song wizard specific pages. + """ + self.searchingPage = QtGui.QWizardPage() + self.searchingPage.setObjectName('searchingPage') + self.verticalLayout = QtGui.QVBoxLayout(self.searchingPage) + self.verticalLayout.setObjectName('verticalLayout') + self.duplicateSearchProgressBar = QtGui.QProgressBar(self.searchingPage) + self.duplicateSearchProgressBar.setObjectName(u'duplicateSearchProgressBar') + self.verticalLayout.addWidget(self.duplicateSearchProgressBar) + self.foundDuplicatesEdit = QtGui.QPlainTextEdit(self.searchingPage) + self.foundDuplicatesEdit.setUndoRedoEnabled(False) + self.foundDuplicatesEdit.setReadOnly(True) + self.foundDuplicatesEdit.setObjectName('foundDuplicatesEdit') + self.verticalLayout.addWidget(self.foundDuplicatesEdit) + self.addPage(self.searchingPage) + self.reviewPage = QtGui.QWizardPage() + self.reviewPage.setObjectName('reviewPage') + self.addPage(self.reviewPage) + + def retranslateUi(self): + """ + Song wizard localisation. + """ + self.setWindowTitle(translate('Wizard', 'Wizard')) + self.titleLabel.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui', + 'Welcome to the Duplicate Song Removal Wizard')) + self.informationLabel.setText(translate("Wizard", + 'This wizard will help you to remove duplicate songs from the song database.')) + self.searchingPage.setTitle(translate('Wizard', 'Searching for doubles')) + self.searchingPage.setSubTitle(translate('Wizard', 'The song database is searched for double songs.')) + + def customPageChanged(self, pageId): + """ + Called when changing to a page other than the progress page. + """ + pass + + def onAddButtonClicked(self): + pass + + def onRemoveButtonClicked(self): + pass + + def setDefaults(self): + """ + Set default form values for the song import wizard. + """ + self.restart() + self.foundDuplicatesEdit.clear() + + def performWizard(self): + """ + Perform the actual import. This method pulls in the correct importer + class, and then runs the ``doImport`` method of the importer to do + the actual importing. + """ + pass diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 726cbfbf5..b19eee5e7 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -46,6 +46,8 @@ from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder +from openlp.plugins.songs.forms.duplicatesongremovalform import \ + DuplicateSongRemovalForm log = logging.getLogger(__name__) @@ -184,6 +186,10 @@ class SongsPlugin(Plugin): QtGui.QMessageBox.information(self.formParent, "Double found", str(innerSongCounter) + " " + str(outerSongCounter)) + if not hasattr(self, u'duplicate_removal_wizard'): + self.duplicate_removal_wizard = \ + DuplicateSongRemovalForm(self.formParent, self) + self.duplicate_removal_wizard.exec_() def onSongImportItemClicked(self): if self.mediaItem: From 3a9820528ad0b1b26d67261a893b382485579b50 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Wed, 9 Jan 2013 00:17:00 +0100 Subject: [PATCH 005/235] Added detection logic to wizard. --- .../songs/forms/duplicatesongremovalform.py | 19 +++++++++++- openlp/plugins/songs/songsplugin.py | 29 +++++++++---------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 33cd64b4a..3e58a1757 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -38,7 +38,9 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import Receiver, Settings, SettingsManager, translate from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings +from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect +from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder log = logging.getLogger(__name__) @@ -88,6 +90,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.verticalLayout.setObjectName('verticalLayout') self.duplicateSearchProgressBar = QtGui.QProgressBar(self.searchingPage) self.duplicateSearchProgressBar.setObjectName(u'duplicateSearchProgressBar') + self.duplicateSearchProgressBar.setFormat(WizardStrings.PercentSymbolFormat) self.verticalLayout.addWidget(self.duplicateSearchProgressBar) self.foundDuplicatesEdit = QtGui.QPlainTextEdit(self.searchingPage) self.foundDuplicatesEdit.setUndoRedoEnabled(False) @@ -115,7 +118,20 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Called when changing to a page other than the progress page. """ - pass + if self.page(pageId) == self.searchingPage: + maxSongs = self.plugin.manager.get_object_count(Song) + if maxSongs == 0 or maxSongs == 1: + return + # with x songs we have x*(x-1)/2 comparisons + maxProgressCount = maxSongs*(maxSongs-1)/2 + self.duplicateSearchProgressBar.setMaximum(maxProgressCount) + songs = self.plugin.manager.get_all_objects(Song) + for outerSongCounter in range(maxSongs-1): + for innerSongCounter in range(outerSongCounter+1, maxSongs): + doubleFinder = DuplicateSongFinder() + if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): + self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) + self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) def onAddButtonClicked(self): pass @@ -128,6 +144,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): Set default form values for the song import wizard. """ self.restart() + self.duplicateSearchProgressBar.setValue(0) self.foundDuplicatesEdit.clear() def performWizard(self): diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index b19eee5e7..b006eb19a 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -45,7 +45,6 @@ from openlp.plugins.songs.lib import clean_song, upgrade, SongMediaItem, \ from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport -from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder from openlp.plugins.songs.forms.duplicatesongremovalform import \ DuplicateSongRemovalForm @@ -172,20 +171,20 @@ class SongsPlugin(Plugin): """ Search for duplicates in the song database. """ - maxSongs = self.manager.get_object_count(Song) - if maxSongs == 0: - return - QtGui.QMessageBox.information(self.formParent, - "Find duplicates called", "Called...") - songs = self.manager.get_all_objects(Song) - for outerSongCounter in range(maxSongs-1): - for innerSongCounter in range(outerSongCounter+1, maxSongs): - doubleFinder = DuplicateSongFinder() - if doubleFinder.songsProbablyEqual(songs[outerSongCounter], - songs[innerSongCounter]): - QtGui.QMessageBox.information(self.formParent, - "Double found", str(innerSongCounter) + " " + - str(outerSongCounter)) + #maxSongs = self.manager.get_object_count(Song) + #if maxSongs == 0: + # return + #QtGui.QMessageBox.information(self.formParent, + # "Find duplicates called", "Called...") + #songs = self.manager.get_all_objects(Song) + #for outerSongCounter in range(maxSongs-1): + # for innerSongCounter in range(outerSongCounter+1, maxSongs): + # doubleFinder = DuplicateSongFinder() + # if doubleFinder.songsProbablyEqual(songs[outerSongCounter], + # songs[innerSongCounter]): + # QtGui.QMessageBox.information(self.formParent, + # "Double found", str(innerSongCounter) + " " + + # str(outerSongCounter)) if not hasattr(self, u'duplicate_removal_wizard'): self.duplicate_removal_wizard = \ DuplicateSongRemovalForm(self.formParent, self) From 1640bc653161b203e293eff9a7dd9d852e66b166 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 10 Jan 2013 23:39:32 +0100 Subject: [PATCH 006/235] Add images/song_delete.png to resources. --- resources/images/openlp-2.qrc | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index beebcff67..c68bbc4f5 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -20,6 +20,7 @@ song_author_edit.png song_topic_edit.png song_book_edit.png + song_delete.png bibles_search_text.png From 709c398205b55bfc527ec3c0a00b09445feba3a1 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 10 Jan 2013 23:49:40 +0100 Subject: [PATCH 007/235] First half of song removal review UI. --- .../songs/forms/duplicatesongremovalform.py | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 3e58a1757..122c0f5b2 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 ############################################################################### # OpenLP - Open Source Lyrics Projection # @@ -35,7 +35,7 @@ import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import Receiver, Settings, SettingsManager, translate +from openlp.core.lib import Receiver, Settings, SettingsManager, translate, build_icon from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song @@ -86,20 +86,30 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ self.searchingPage = QtGui.QWizardPage() self.searchingPage.setObjectName('searchingPage') - self.verticalLayout = QtGui.QVBoxLayout(self.searchingPage) - self.verticalLayout.setObjectName('verticalLayout') + self.searchingVerticalLayout = QtGui.QVBoxLayout(self.searchingPage) + self.searchingVerticalLayout.setObjectName('searchingVerticalLayout') self.duplicateSearchProgressBar = QtGui.QProgressBar(self.searchingPage) self.duplicateSearchProgressBar.setObjectName(u'duplicateSearchProgressBar') self.duplicateSearchProgressBar.setFormat(WizardStrings.PercentSymbolFormat) - self.verticalLayout.addWidget(self.duplicateSearchProgressBar) + self.searchingVerticalLayout.addWidget(self.duplicateSearchProgressBar) self.foundDuplicatesEdit = QtGui.QPlainTextEdit(self.searchingPage) self.foundDuplicatesEdit.setUndoRedoEnabled(False) self.foundDuplicatesEdit.setReadOnly(True) self.foundDuplicatesEdit.setObjectName('foundDuplicatesEdit') - self.verticalLayout.addWidget(self.foundDuplicatesEdit) + self.searchingVerticalLayout.addWidget(self.foundDuplicatesEdit) self.addPage(self.searchingPage) self.reviewPage = QtGui.QWizardPage() self.reviewPage.setObjectName('reviewPage') + self.headerVerticalLayout = QtGui.QVBoxLayout(self.reviewPage) + self.headerVerticalLayout.setObjectName('headerVerticalLayout') + self.reviewCounterLabel = QtGui.QLabel(self.reviewPage) + self.reviewCounterLabel.setObjectName('reviewCounterLabel') + self.headerVerticalLayout.addWidget(self.reviewCounterLabel) + self.songsHorizontalLayout = QtGui.QHBoxLayout() + self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') + self.songReviewWidget = SongReviewWidget(self.reviewPage) + self.songsHorizontalLayout.addWidget(self.songReviewWidget) + self.headerVerticalLayout.addLayout(self.songsHorizontalLayout) self.addPage(self.reviewPage) def retranslateUi(self): @@ -113,6 +123,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): 'This wizard will help you to remove duplicate songs from the song database.')) self.searchingPage.setTitle(translate('Wizard', 'Searching for doubles')) self.searchingPage.setSubTitle(translate('Wizard', 'The song database is searched for double songs.')) + self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs')) + self.reviewPage.setSubTitle(translate('Wizard', + 'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) def customPageChanged(self, pageId): """ @@ -154,3 +167,23 @@ class DuplicateSongRemovalForm(OpenLPWizard): the actual importing. """ pass + +class SongReviewWidget(QtGui.QWidget): + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + self.setupUi() + self.retranslateUi() + + def setupUi(self): + self.songVerticalLayout = QtGui.QVBoxLayout(self) + self.songVerticalLayout.setObjectName('songVerticalLayout') + self.songGroupBox = QtGui.QGroupBox(self) + self.songGroupBox.setObjectName('songGroupBox') + self.songVerticalLayout.addWidget(self.songGroupBox) + self.songRemoveButton = QtGui.QPushButton(self) + self.songRemoveButton.setObjectName('songRemoveButton') + self.songRemoveButton.setIcon(build_icon(u':/songs/song_delete.png')) + self.songVerticalLayout.addWidget(self.songRemoveButton) + + def retranslateUi(self): + self.songRemoveButton.setText(u'Remove') From bb23cd133d0579af532895cf9dba2af532fcbf24 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Fri, 11 Jan 2013 00:08:52 +0100 Subject: [PATCH 008/235] Some more GUI. --- .../songs/forms/duplicatesongremovalform.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 122c0f5b2..a36f951a7 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -179,6 +179,21 @@ class SongReviewWidget(QtGui.QWidget): self.songVerticalLayout.setObjectName('songVerticalLayout') self.songGroupBox = QtGui.QGroupBox(self) self.songGroupBox.setObjectName('songGroupBox') + self.songContentVerticalLayout = QtGui.QVBoxLayout(self.songGroupBox) + self.songContentVerticalLayout.setObjectName('songContentVerticalLayout') + self.songInfoFormLayout = QtGui.QFormLayout() + self.songInfoFormLayout.setObjectName('songInfoFormLayout') + #add ccli number, name, altname, authors, ... here + self.songNameLabel = QtGui.QLabel(self) + self.songNameLabel.setObjectName('songNameLabel') + self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songNameLabel) + self.songNameContent = QtGui.QLabel(self) + self.songNameContent.setObjectName('songNameContent') + self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songNameContent) + self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) + self.songVerseButton = QtGui.QPushButton(self) + self.songVerseButton.setObjectName('songVerseButton') + self.songContentVerticalLayout.addWidget(self.songVerseButton) self.songVerticalLayout.addWidget(self.songGroupBox) self.songRemoveButton = QtGui.QPushButton(self) self.songRemoveButton.setObjectName('songRemoveButton') @@ -187,3 +202,5 @@ class SongReviewWidget(QtGui.QWidget): def retranslateUi(self): self.songRemoveButton.setText(u'Remove') + self.songNameLabel.setText(u'Name:') + From cd7a57028b75a469720e2d912ec037ec007dbd81 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Fri, 11 Jan 2013 18:45:08 +0100 Subject: [PATCH 009/235] Fill song review widgets with useful content. --- .../songs/forms/duplicatesongremovalform.py | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index a36f951a7..0a244ecb9 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -36,6 +36,7 @@ import os from PyQt4 import QtCore, QtGui from openlp.core.lib import Receiver, Settings, SettingsManager, translate, build_icon +from openlp.core.lib.db import Manager from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song @@ -69,7 +70,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Song wizard specific initialisation. """ - pass + self.duplicateSongList = [] def customSignals(self): """ @@ -107,8 +108,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.headerVerticalLayout.addWidget(self.reviewCounterLabel) self.songsHorizontalLayout = QtGui.QHBoxLayout() self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') - self.songReviewWidget = SongReviewWidget(self.reviewPage) - self.songsHorizontalLayout.addWidget(self.songReviewWidget) self.headerVerticalLayout.addLayout(self.songsHorizontalLayout) self.addPage(self.reviewPage) @@ -143,8 +142,36 @@ class DuplicateSongRemovalForm(OpenLPWizard): for innerSongCounter in range(outerSongCounter+1, maxSongs): doubleFinder = DuplicateSongFinder() if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): + self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) + elif self.page(pageId) == self.reviewPage: + print "asdf" + print self.duplicateSongList + for duplicates in self.duplicateSongList[0:1]: + print "qwer" + for duplicate in duplicates: + print "zxcv" + songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) + self.songsHorizontalLayout.addWidget(songReviewWidget) + + def addDuplicatesToSongList(self, searchSong, duplicateSong): + duplicateGroupFound = False + for duplicates in self.duplicateSongList: + #skip the first song in the duplicate lists, since the first one has to be an earlier song + for duplicate in duplicates[1:]: + if duplicate == searchSong: + duplicates.append(duplicateSong) + duplicateGroupFound = True + break + elif duplicate == duplicateSong: + duplicates.appen(searchSong) + duplicateGroupFound = True + break + if duplicateGroupFound: + break + if not duplicateGroupFound: + self.duplicateSongList.append([searchSong, duplicateSong]) def onAddButtonClicked(self): pass @@ -169,8 +196,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): pass class SongReviewWidget(QtGui.QWidget): - def __init__(self, parent): + def __init__(self, parent, song): QtGui.QWidget.__init__(self, parent) + self.song = song self.setupUi() self.retranslateUi() @@ -189,6 +217,7 @@ class SongReviewWidget(QtGui.QWidget): self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songNameLabel) self.songNameContent = QtGui.QLabel(self) self.songNameContent.setObjectName('songNameContent') + self.songNameContent.setText(self.song.title) self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songNameContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) self.songVerseButton = QtGui.QPushButton(self) From 1b641da997982a619401eda9d281baca8a8f6791 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 12 Jan 2013 22:22:44 +0100 Subject: [PATCH 010/235] Add authors to review page. --- .../songs/forms/duplicatesongremovalform.py | 73 ++++++++++++++++--- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 0a244ecb9..e32dce30a 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -146,12 +146,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) elif self.page(pageId) == self.reviewPage: - print "asdf" - print self.duplicateSongList for duplicates in self.duplicateSongList[0:1]: - print "qwer" for duplicate in duplicates: - print "zxcv" songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) self.songsHorizontalLayout.addWidget(songReviewWidget) @@ -212,13 +208,60 @@ class SongReviewWidget(QtGui.QWidget): self.songInfoFormLayout = QtGui.QFormLayout() self.songInfoFormLayout.setObjectName('songInfoFormLayout') #add ccli number, name, altname, authors, ... here - self.songNameLabel = QtGui.QLabel(self) - self.songNameLabel.setObjectName('songNameLabel') - self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songNameLabel) - self.songNameContent = QtGui.QLabel(self) - self.songNameContent.setObjectName('songNameContent') - self.songNameContent.setText(self.song.title) - self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songNameContent) + self.songTitleLabel = QtGui.QLabel(self) + self.songTitleLabel.setObjectName('songTitleLabel') + self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songTitleLabel) + self.songTitleContent = QtGui.QLabel(self) + self.songTitleContent.setObjectName('songTitleContent') + self.songTitleContent.setText(self.song.title) + self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songTitleContent) + self.songAlternateTitleLabel = QtGui.QLabel(self) + self.songAlternateTitleLabel.setObjectName('songAlternateTitleLabel') + self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.songAlternateTitleLabel) + self.songAlternateTitleContent = QtGui.QLabel(self) + self.songAlternateTitleContent.setObjectName('songAlternateTitleContent') + self.songAlternateTitleContent.setText(self.song.alternate_title) + self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.songAlternateTitleContent) + self.songCCLINumberLabel = QtGui.QLabel(self) + self.songCCLINumberLabel.setObjectName('songCCLINumberLabel') + self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.songCCLINumberLabel) + self.songCCLINumberContent = QtGui.QLabel(self) + self.songCCLINumberContent.setObjectName('songCCLINumberContent') + self.songCCLINumberContent.setText(self.song.ccli_number) + self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) + self.songVerseOrderLabel = QtGui.QLabel(self) + self.songVerseOrderLabel.setObjectName('songVerseOrderLabel') + self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) + self.songVerseOrderContent = QtGui.QLabel(self) + self.songVerseOrderContent.setObjectName('songVerseOrderContent') + self.songVerseOrderContent.setText(self.song.verse_order) + self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) + self.songCopyrightLabel = QtGui.QLabel(self) + self.songCopyrightLabel.setObjectName('songCopyrightLabel') + self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) + self.songCopyrightContent = QtGui.QLabel(self) + self.songCopyrightContent.setObjectName('songCopyrightContent') + self.songCopyrightContent.setText(self.song.copyright) + self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) + self.songCommentsLabel = QtGui.QLabel(self) + self.songCommentsLabel.setObjectName('songCommentsLabel') + self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) + self.songCommentsContent = QtGui.QLabel(self) + self.songCommentsContent.setObjectName('songCommentsContent') + self.songCommentsContent.setText(self.song.comments) + self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songCommentsContent) + self.songAuthorsLabel = QtGui.QLabel(self) + self.songAuthorsLabel.setObjectName('songAuthorsLabel') + self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) + self.songAuthorsContent = QtGui.QLabel(self) + self.songAuthorsContent.setObjectName('songAuthorsContent') + authorsText = u'' + for author in self.song.authors: + authorsText += author.display_name + ', ' + if authorsText: + authorsText = authorsText[:-2] + self.songAuthorsContent.setText(authorsText) + self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) self.songVerseButton = QtGui.QPushButton(self) self.songVerseButton.setObjectName('songVerseButton') @@ -231,5 +274,11 @@ class SongReviewWidget(QtGui.QWidget): def retranslateUi(self): self.songRemoveButton.setText(u'Remove') - self.songNameLabel.setText(u'Name:') + self.songTitleLabel.setText(u'Title:') + self.songAlternateTitleLabel.setText(u'Alternate Title:') + self.songCCLINumberLabel.setText(u'CCLI Number:') + self.songVerseOrderLabel.setText(u'Verse Order:') + self.songCopyrightLabel.setText(u'Copyright:') + self.songCommentsLabel.setText(u'Comments:') + self.songAuthorsLabel.setText(u'Authors:') From 90c83dbbe6abd04fa1d9036a732e5a28440ed099 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sun, 13 Jan 2013 23:59:58 +0100 Subject: [PATCH 011/235] Got layout to sort of work. --- .../songs/forms/duplicatesongremovalform.py | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index e32dce30a..99b19bb1b 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -106,9 +106,25 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.reviewCounterLabel = QtGui.QLabel(self.reviewPage) self.reviewCounterLabel.setObjectName('reviewCounterLabel') self.headerVerticalLayout.addWidget(self.reviewCounterLabel) - self.songsHorizontalLayout = QtGui.QHBoxLayout() + + #self.songsHorizontalLayout = QtGui.QHBoxLayout() + #self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') + #self.headerVerticalLayout.addLayout(self.songsHorizontalLayout) + + self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) + self.songsHorizontalScrollArea.setObjectName('songsHorizontalScrollArea') + self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.songsHorizontalScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) + self.songsHorizontalScrollArea.setWidgetResizable(True) + self.songsHorizontalSongsWidget = QtGui.QWidget(self.songsHorizontalScrollArea) + self.songsHorizontalSongsWidget.setObjectName('songsHorizontalSongsWidget') + self.songsHorizontalLayout = QtGui.QHBoxLayout(self.songsHorizontalSongsWidget) self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') - self.headerVerticalLayout.addLayout(self.songsHorizontalLayout) + self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) + self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) + self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) + self.addPage(self.reviewPage) def retranslateUi(self): @@ -146,10 +162,15 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) elif self.page(pageId) == self.reviewPage: + #a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 + self.songsHorizontalLayout.addStretch() + self.songsHorizontalLayout.addStretch() for duplicates in self.duplicateSongList[0:1]: for duplicate in duplicates: songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) self.songsHorizontalLayout.addWidget(songReviewWidget) + self.songsHorizontalLayout.addStretch() + self.songsHorizontalLayout.addStretch() def addDuplicatesToSongList(self, searchSong, duplicateSong): duplicateGroupFound = False @@ -161,7 +182,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): duplicateGroupFound = True break elif duplicate == duplicateSong: - duplicates.appen(searchSong) + duplicates.append(searchSong) duplicateGroupFound = True break if duplicateGroupFound: @@ -200,61 +221,80 @@ class SongReviewWidget(QtGui.QWidget): def setupUi(self): self.songVerticalLayout = QtGui.QVBoxLayout(self) - self.songVerticalLayout.setObjectName('songVerticalLayout') + self.songVerticalLayout.setObjectName(u'songVerticalLayout') self.songGroupBox = QtGui.QGroupBox(self) - self.songGroupBox.setObjectName('songGroupBox') - self.songContentVerticalLayout = QtGui.QVBoxLayout(self.songGroupBox) - self.songContentVerticalLayout.setObjectName('songContentVerticalLayout') + self.songGroupBox.setObjectName(u'songGroupBox') + self.songGroupBox.setMinimumWidth(300) + self.songGroupBox.setMaximumWidth(300) + #self.songGroupBox.setMinimumHeight(300) + #self.songGroupBox.setMaximumHeight(300) + self.songGroupBoxLayout = QtGui.QVBoxLayout(self.songGroupBox) + self.songGroupBoxLayout.setObjectName(u'songGroupBoxLayout') + self.songScrollArea = QtGui.QScrollArea(self) + self.songScrollArea.setObjectName(u'songScrollArea') + self.songScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.songScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.songScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) + self.songScrollArea.setWidgetResizable(True) + self.songContentWidget = QtGui.QWidget(self.songScrollArea) + self.songContentWidget.setObjectName(u'songContentWidget') + #self.songContentWidget.setMinimumWidth(300) + #self.songContentWidget.setMaximumWidth(300) + self.songContentVerticalLayout = QtGui.QVBoxLayout(self.songContentWidget) + self.songContentVerticalLayout.setObjectName(u'songContentVerticalLayout') + self.songContentVerticalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songInfoFormLayout = QtGui.QFormLayout() - self.songInfoFormLayout.setObjectName('songInfoFormLayout') + self.songInfoFormLayout.setObjectName(u'songInfoFormLayout') #add ccli number, name, altname, authors, ... here self.songTitleLabel = QtGui.QLabel(self) - self.songTitleLabel.setObjectName('songTitleLabel') + self.songTitleLabel.setObjectName(u'songTitleLabel') self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songTitleLabel) self.songTitleContent = QtGui.QLabel(self) - self.songTitleContent.setObjectName('songTitleContent') + self.songTitleContent.setObjectName(u'songTitleContent') self.songTitleContent.setText(self.song.title) self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songTitleContent) self.songAlternateTitleLabel = QtGui.QLabel(self) - self.songAlternateTitleLabel.setObjectName('songAlternateTitleLabel') + self.songAlternateTitleLabel.setObjectName(u'songAlternateTitleLabel') self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.songAlternateTitleLabel) self.songAlternateTitleContent = QtGui.QLabel(self) - self.songAlternateTitleContent.setObjectName('songAlternateTitleContent') + self.songAlternateTitleContent.setObjectName(u'songAlternateTitleContent') self.songAlternateTitleContent.setText(self.song.alternate_title) self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.songAlternateTitleContent) self.songCCLINumberLabel = QtGui.QLabel(self) - self.songCCLINumberLabel.setObjectName('songCCLINumberLabel') + self.songCCLINumberLabel.setObjectName(u'songCCLINumberLabel') self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.songCCLINumberLabel) self.songCCLINumberContent = QtGui.QLabel(self) - self.songCCLINumberContent.setObjectName('songCCLINumberContent') + self.songCCLINumberContent.setObjectName(u'songCCLINumberContent') self.songCCLINumberContent.setText(self.song.ccli_number) self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) self.songVerseOrderLabel = QtGui.QLabel(self) - self.songVerseOrderLabel.setObjectName('songVerseOrderLabel') + self.songVerseOrderLabel.setObjectName(u'songVerseOrderLabel') self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) self.songVerseOrderContent = QtGui.QLabel(self) - self.songVerseOrderContent.setObjectName('songVerseOrderContent') + self.songVerseOrderContent.setObjectName(u'songVerseOrderContent') self.songVerseOrderContent.setText(self.song.verse_order) self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) self.songCopyrightLabel = QtGui.QLabel(self) - self.songCopyrightLabel.setObjectName('songCopyrightLabel') + self.songCopyrightLabel.setObjectName(u'songCopyrightLabel') self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) self.songCopyrightContent = QtGui.QLabel(self) - self.songCopyrightContent.setObjectName('songCopyrightContent') + self.songCopyrightContent.setObjectName(u'songCopyrightContent') + self.songCopyrightContent.setWordWrap(True) self.songCopyrightContent.setText(self.song.copyright) self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) self.songCommentsLabel = QtGui.QLabel(self) - self.songCommentsLabel.setObjectName('songCommentsLabel') + self.songCommentsLabel.setObjectName(u'songCommentsLabel') self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) self.songCommentsContent = QtGui.QLabel(self) - self.songCommentsContent.setObjectName('songCommentsContent') + self.songCommentsContent.setObjectName(u'songCommentsContent') self.songCommentsContent.setText(self.song.comments) self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songCommentsContent) self.songAuthorsLabel = QtGui.QLabel(self) - self.songAuthorsLabel.setObjectName('songAuthorsLabel') + self.songAuthorsLabel.setObjectName(u'songAuthorsLabel') self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) self.songAuthorsContent = QtGui.QLabel(self) - self.songAuthorsContent.setObjectName('songAuthorsContent') + self.songAuthorsContent.setObjectName(u'songAuthorsContent') + self.songAuthorsContent.setWordWrap(True) authorsText = u'' for author in self.song.authors: authorsText += author.display_name + ', ' @@ -264,11 +304,14 @@ class SongReviewWidget(QtGui.QWidget): self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) self.songVerseButton = QtGui.QPushButton(self) - self.songVerseButton.setObjectName('songVerseButton') + self.songVerseButton.setObjectName(u'songVerseButton') self.songContentVerticalLayout.addWidget(self.songVerseButton) + self.songScrollArea.setWidget(self.songContentWidget) + self.songGroupBoxLayout.addWidget(self.songScrollArea) + #self.songGroupBoxLayout.addStretch() self.songVerticalLayout.addWidget(self.songGroupBox) self.songRemoveButton = QtGui.QPushButton(self) - self.songRemoveButton.setObjectName('songRemoveButton') + self.songRemoveButton.setObjectName(u'songRemoveButton') self.songRemoveButton.setIcon(build_icon(u':/songs/song_delete.png')) self.songVerticalLayout.addWidget(self.songRemoveButton) From bdac939dcde13daaf2317490c1f4f2357ad33138 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 14 Jan 2013 21:58:09 +0100 Subject: [PATCH 012/235] Lots of polishing. --- .../songs/forms/duplicatesongremovalform.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 99b19bb1b..20a1954cb 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -35,12 +35,12 @@ import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import Receiver, Settings, SettingsManager, translate, build_icon +from openlp.core.lib import translate, build_icon from openlp.core.lib.db import Manager from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.plugins.songs.lib.db import Song -from openlp.plugins.songs.lib.importer import SongFormat, SongFormatSelect +from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder log = logging.getLogger(__name__) @@ -106,25 +106,21 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.reviewCounterLabel = QtGui.QLabel(self.reviewPage) self.reviewCounterLabel.setObjectName('reviewCounterLabel') self.headerVerticalLayout.addWidget(self.reviewCounterLabel) - - #self.songsHorizontalLayout = QtGui.QHBoxLayout() - #self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') - #self.headerVerticalLayout.addLayout(self.songsHorizontalLayout) - self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) self.songsHorizontalScrollArea.setObjectName('songsHorizontalScrollArea') self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.songsHorizontalScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) self.songsHorizontalScrollArea.setWidgetResizable(True) + self.songsHorizontalScrollArea.setStyleSheet("QScrollArea#songsHorizontalScrollArea {background-color:transparent;}"); self.songsHorizontalSongsWidget = QtGui.QWidget(self.songsHorizontalScrollArea) self.songsHorizontalSongsWidget.setObjectName('songsHorizontalSongsWidget') + self.songsHorizontalSongsWidget.setStyleSheet("QWidget#songsHorizontalSongsWidget {background-color:transparent;}"); self.songsHorizontalLayout = QtGui.QHBoxLayout(self.songsHorizontalSongsWidget) self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) - self.addPage(self.reviewPage) def retranslateUi(self): @@ -226,8 +222,6 @@ class SongReviewWidget(QtGui.QWidget): self.songGroupBox.setObjectName(u'songGroupBox') self.songGroupBox.setMinimumWidth(300) self.songGroupBox.setMaximumWidth(300) - #self.songGroupBox.setMinimumHeight(300) - #self.songGroupBox.setMaximumHeight(300) self.songGroupBoxLayout = QtGui.QVBoxLayout(self.songGroupBox) self.songGroupBoxLayout.setObjectName(u'songGroupBoxLayout') self.songScrollArea = QtGui.QScrollArea(self) @@ -238,14 +232,11 @@ class SongReviewWidget(QtGui.QWidget): self.songScrollArea.setWidgetResizable(True) self.songContentWidget = QtGui.QWidget(self.songScrollArea) self.songContentWidget.setObjectName(u'songContentWidget') - #self.songContentWidget.setMinimumWidth(300) - #self.songContentWidget.setMaximumWidth(300) self.songContentVerticalLayout = QtGui.QVBoxLayout(self.songContentWidget) self.songContentVerticalLayout.setObjectName(u'songContentVerticalLayout') self.songContentVerticalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songInfoFormLayout = QtGui.QFormLayout() self.songInfoFormLayout.setObjectName(u'songInfoFormLayout') - #add ccli number, name, altname, authors, ... here self.songTitleLabel = QtGui.QLabel(self) self.songTitleLabel.setObjectName(u'songTitleLabel') self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songTitleLabel) @@ -303,17 +294,30 @@ class SongReviewWidget(QtGui.QWidget): self.songAuthorsContent.setText(authorsText) self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) + + + + songXml = SongXML() + verses = songXml.get_verses(self.song.lyrics) + print verses + + + self.songVerseButton = QtGui.QPushButton(self) self.songVerseButton.setObjectName(u'songVerseButton') self.songContentVerticalLayout.addWidget(self.songVerseButton) + + + + self.songContentVerticalLayout.addStretch() self.songScrollArea.setWidget(self.songContentWidget) self.songGroupBoxLayout.addWidget(self.songScrollArea) - #self.songGroupBoxLayout.addStretch() self.songVerticalLayout.addWidget(self.songGroupBox) self.songRemoveButton = QtGui.QPushButton(self) self.songRemoveButton.setObjectName(u'songRemoveButton') self.songRemoveButton.setIcon(build_icon(u':/songs/song_delete.png')) - self.songVerticalLayout.addWidget(self.songRemoveButton) + self.songRemoveButton.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.songVerticalLayout.addWidget(self.songRemoveButton, alignment = QtCore.Qt.AlignHCenter) def retranslateUi(self): self.songRemoveButton.setText(u'Remove') From abd2d783aa4fc19a9e9605a2a77c6cb7b4cd5cae Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 14 Jan 2013 23:02:32 +0100 Subject: [PATCH 013/235] Add verses to review page. --- .../songs/forms/duplicatesongremovalform.py | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 20a1954cb..63f018b01 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -258,31 +258,24 @@ class SongReviewWidget(QtGui.QWidget): self.songCCLINumberContent.setObjectName(u'songCCLINumberContent') self.songCCLINumberContent.setText(self.song.ccli_number) self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) - self.songVerseOrderLabel = QtGui.QLabel(self) - self.songVerseOrderLabel.setObjectName(u'songVerseOrderLabel') - self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) - self.songVerseOrderContent = QtGui.QLabel(self) - self.songVerseOrderContent.setObjectName(u'songVerseOrderContent') - self.songVerseOrderContent.setText(self.song.verse_order) - self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) self.songCopyrightLabel = QtGui.QLabel(self) self.songCopyrightLabel.setObjectName(u'songCopyrightLabel') - self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) + self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) self.songCopyrightContent = QtGui.QLabel(self) self.songCopyrightContent.setObjectName(u'songCopyrightContent') self.songCopyrightContent.setWordWrap(True) self.songCopyrightContent.setText(self.song.copyright) - self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) + self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) self.songCommentsLabel = QtGui.QLabel(self) self.songCommentsLabel.setObjectName(u'songCommentsLabel') - self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) + self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) self.songCommentsContent = QtGui.QLabel(self) self.songCommentsContent.setObjectName(u'songCommentsContent') self.songCommentsContent.setText(self.song.comments) - self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songCommentsContent) + self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCommentsContent) self.songAuthorsLabel = QtGui.QLabel(self) self.songAuthorsLabel.setObjectName(u'songAuthorsLabel') - self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) + self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) self.songAuthorsContent = QtGui.QLabel(self) self.songAuthorsContent.setObjectName(u'songAuthorsContent') self.songAuthorsContent.setWordWrap(True) @@ -292,23 +285,26 @@ class SongReviewWidget(QtGui.QWidget): if authorsText: authorsText = authorsText[:-2] self.songAuthorsContent.setText(authorsText) - self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) + self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) + self.songVerseOrderLabel = QtGui.QLabel(self) + self.songVerseOrderLabel.setObjectName(u'songVerseOrderLabel') + self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) + self.songVerseOrderContent = QtGui.QLabel(self) + self.songVerseOrderContent.setObjectName(u'songVerseOrderContent') + self.songVerseOrderContent.setText(self.song.verse_order) + self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) - - - + self.songInfoVerseGroupBox = QtGui.QGroupBox(self.songGroupBox) + self.songInfoVerseGroupBox.setObjectName(u'songInfoVerseGroupBox') + self.songInfoVerseGroupBoxLayout = QtGui.QFormLayout(self.songInfoVerseGroupBox) songXml = SongXML() verses = songXml.get_verses(self.song.lyrics) - print verses - - - - self.songVerseButton = QtGui.QPushButton(self) - self.songVerseButton.setObjectName(u'songVerseButton') - self.songContentVerticalLayout.addWidget(self.songVerseButton) - - - + for verse in verses: + verseMarker = verse[0]['type'] + verse[0]['label'] + verseLabel = QtGui.QLabel(self.songInfoVerseGroupBox) + verseLabel.setText(verse[1]) + self.songInfoVerseGroupBoxLayout.addRow(verseMarker, verseLabel) + self.songContentVerticalLayout.addWidget(self.songInfoVerseGroupBox) self.songContentVerticalLayout.addStretch() self.songScrollArea.setWidget(self.songContentWidget) self.songGroupBoxLayout.addWidget(self.songScrollArea) @@ -328,4 +324,5 @@ class SongReviewWidget(QtGui.QWidget): self.songCopyrightLabel.setText(u'Copyright:') self.songCommentsLabel.setText(u'Comments:') self.songAuthorsLabel.setText(u'Authors:') + self.songInfoVerseGroupBox.setTitle(u'Verses') From 6f50adc8c6ea2257cae53fa60880ccfbc1ec1d67 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 15 Jan 2013 21:48:05 +0100 Subject: [PATCH 014/235] Implement deletion of songs. --- .../songs/forms/duplicatesongremovalform.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 63f018b01..1584cde1b 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -39,7 +39,8 @@ from openlp.core.lib import translate, build_icon from openlp.core.lib.db import Manager from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings -from openlp.plugins.songs.lib.db import Song +from openlp.core.utils import AppLocation +from openlp.plugins.songs.lib.db import Song, MediaFile from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder @@ -164,6 +165,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): for duplicates in self.duplicateSongList[0:1]: for duplicate in duplicates: songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) + QtCore.QObject.connect(songReviewWidget, + QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), + self.removeButtonClicked) self.songsHorizontalLayout.addWidget(songReviewWidget) self.songsHorizontalLayout.addStretch() self.songsHorizontalLayout.addStretch() @@ -208,12 +212,37 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ pass + def removeButtonClicked(self, songReviewWidget): + #TODO: turn this method into a slot + #check if last song + #disable remove button + #remove GUI elements + #remove song + item_id = songReviewWidget.song.id + media_files = self.plugin.manager.get_all_objects(MediaFile, + MediaFile.song_id == item_id) + for media_file in media_files: + try: + os.remove(media_file.file_name) + except: + log.exception('Could not remove file: %s', + media_file.file_name) + try: + save_path = os.path.join(AppLocation.get_section_data_path( + self.plugin.name), 'audio', str(item_id)) + if os.path.exists(save_path): + os.rmdir(save_path) + except OSError: + log.exception(u'Could not remove directory: %s', save_path) + self.plugin.manager.delete_object(Song, item_id) + class SongReviewWidget(QtGui.QWidget): def __init__(self, parent, song): QtGui.QWidget.__init__(self, parent) self.song = song self.setupUi() self.retranslateUi() + QtCore.QObject.connect(self.songRemoveButton, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) def setupUi(self): self.songVerticalLayout = QtGui.QVBoxLayout(self) @@ -243,6 +272,7 @@ class SongReviewWidget(QtGui.QWidget): self.songTitleContent = QtGui.QLabel(self) self.songTitleContent.setObjectName(u'songTitleContent') self.songTitleContent.setText(self.song.title) + self.songTitleContent.setWordWrap(True) self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songTitleContent) self.songAlternateTitleLabel = QtGui.QLabel(self) self.songAlternateTitleLabel.setObjectName(u'songAlternateTitleLabel') @@ -250,6 +280,7 @@ class SongReviewWidget(QtGui.QWidget): self.songAlternateTitleContent = QtGui.QLabel(self) self.songAlternateTitleContent.setObjectName(u'songAlternateTitleContent') self.songAlternateTitleContent.setText(self.song.alternate_title) + self.songAlternateTitleContent.setWordWrap(True) self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.songAlternateTitleContent) self.songCCLINumberLabel = QtGui.QLabel(self) self.songCCLINumberLabel.setObjectName(u'songCCLINumberLabel') @@ -257,6 +288,7 @@ class SongReviewWidget(QtGui.QWidget): self.songCCLINumberContent = QtGui.QLabel(self) self.songCCLINumberContent.setObjectName(u'songCCLINumberContent') self.songCCLINumberContent.setText(self.song.ccli_number) + self.songCCLINumberContent.setWordWrap(True) self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) self.songCopyrightLabel = QtGui.QLabel(self) self.songCopyrightLabel.setObjectName(u'songCopyrightLabel') @@ -272,6 +304,7 @@ class SongReviewWidget(QtGui.QWidget): self.songCommentsContent = QtGui.QLabel(self) self.songCommentsContent.setObjectName(u'songCommentsContent') self.songCommentsContent.setText(self.song.comments) + self.songCommentsContent.setWordWrap(True) self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCommentsContent) self.songAuthorsLabel = QtGui.QLabel(self) self.songAuthorsLabel.setObjectName(u'songAuthorsLabel') @@ -292,6 +325,7 @@ class SongReviewWidget(QtGui.QWidget): self.songVerseOrderContent = QtGui.QLabel(self) self.songVerseOrderContent.setObjectName(u'songVerseOrderContent') self.songVerseOrderContent.setText(self.song.verse_order) + self.songVerseOrderContent.setWordWrap(True) self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) self.songInfoVerseGroupBox = QtGui.QGroupBox(self.songGroupBox) @@ -326,3 +360,6 @@ class SongReviewWidget(QtGui.QWidget): self.songAuthorsLabel.setText(u'Authors:') self.songInfoVerseGroupBox.setTitle(u'Verses') + def onRemoveButtonClicked(self): + self.emit(QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self) + From 4aeb91eb6662ad431f5fc783baac2ce0a914ce99 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 15 Jan 2013 23:26:46 +0100 Subject: [PATCH 015/235] Remove GUI for removed songs, grey out remove button for last song. --- .../songs/forms/duplicatesongremovalform.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 1584cde1b..c3a32b98f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -77,10 +77,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Song wizard specific signals. """ - #QtCore.QObject.connect(self.addButton, - # QtCore.SIGNAL(u'clicked()'), self.onAddButtonClicked) - #QtCore.QObject.connect(self.removeButton, - # QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) + QtCore.QObject.connect(self.finishButton, QtCore.SIGNAL(u'clicked()'), self.onWizardExit) + QtCore.QObject.connect(self.cancelButton, QtCore.SIGNAL(u'clicked()'), self.onWizardExit) def addCustomPages(self): """ @@ -190,10 +188,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): if not duplicateGroupFound: self.duplicateSongList.append([searchSong, duplicateSong]) - def onAddButtonClicked(self): - pass - - def onRemoveButtonClicked(self): + def onWizardExit(self): + #refresh the song list + self.plugin.mediaItem.onSearchTextButtonClicked() pass def setDefaults(self): @@ -213,10 +210,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): pass def removeButtonClicked(self, songReviewWidget): - #TODO: turn this method into a slot - #check if last song - #disable remove button - #remove GUI elements #remove song item_id = songReviewWidget.song.id media_files = self.plugin.manager.get_all_objects(MediaFile, @@ -235,6 +228,14 @@ class DuplicateSongRemovalForm(OpenLPWizard): except OSError: log.exception(u'Could not remove directory: %s', save_path) self.plugin.manager.delete_object(Song, item_id) + #remove GUI elements + self.songsHorizontalLayout.removeWidget(songReviewWidget) + songReviewWidget.setParent(None) + #check if we only have one SongReviewWidget left + # 4 stretches + 1 SongReviewWidget = 5 + # the SongReviewWidget is then at position 3 + if self.songsHorizontalLayout.count() == 5: + self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) class SongReviewWidget(QtGui.QWidget): def __init__(self, parent, song): From f0c58777a2a31d1ff578db45df4a2cde28ea4b9e Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 17 Jan 2013 23:01:05 +0100 Subject: [PATCH 016/235] Implemented reviewing all songs instead of just one. --- .../songs/forms/duplicatesongremovalform.py | 97 +++++++++++++++---- openlp/plugins/songs/songsplugin.py | 19 +--- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index c3a32b98f..93611f83a 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -63,6 +63,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``plugin`` The songs plugin. """ + from PyQt4.QtCore import pyqtRemoveInputHook + pyqtRemoveInputHook() + + self.clipboard = plugin.formParent.clipboard OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_importsong.bmp', False) @@ -84,7 +88,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Add song wizard specific pages. """ - self.searchingPage = QtGui.QWizardPage() + #add custom pages + self.searchingPage = SearchWizardPage(self, self.getNextPageForSearchWizardPage) self.searchingPage.setObjectName('searchingPage') self.searchingVerticalLayout = QtGui.QVBoxLayout(self.searchingPage) self.searchingVerticalLayout.setObjectName('searchingVerticalLayout') @@ -97,7 +102,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit.setReadOnly(True) self.foundDuplicatesEdit.setObjectName('foundDuplicatesEdit') self.searchingVerticalLayout.addWidget(self.foundDuplicatesEdit) - self.addPage(self.searchingPage) + self.searchingPageId = self.addPage(self.searchingPage) self.reviewPage = QtGui.QWizardPage() self.reviewPage.setObjectName('reviewPage') self.headerVerticalLayout = QtGui.QVBoxLayout(self.reviewPage) @@ -120,7 +125,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) - self.addPage(self.reviewPage) + self.reviewPageId = self.addPage(self.reviewPage) + self.finalPage = QtGui.QWizardPage() + self.finalPage.setObjectName('finalPage') + self.finalPageId = self.addPage(self.finalPage) def retranslateUi(self): """ @@ -141,7 +149,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Called when changing to a page other than the progress page. """ - if self.page(pageId) == self.searchingPage: + #hide back button + self.button(QtGui.QWizard.BackButton).hide() + + if pageId == self.searchingPageId: maxSongs = self.plugin.manager.get_object_count(Song) if maxSongs == 0 or maxSongs == 1: return @@ -156,19 +167,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) - elif self.page(pageId) == self.reviewPage: - #a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 - self.songsHorizontalLayout.addStretch() - self.songsHorizontalLayout.addStretch() - for duplicates in self.duplicateSongList[0:1]: - for duplicate in duplicates: - songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) - QtCore.QObject.connect(songReviewWidget, - QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), - self.removeButtonClicked) - self.songsHorizontalLayout.addWidget(songReviewWidget) - self.songsHorizontalLayout.addStretch() - self.songsHorizontalLayout.addStretch() + elif pageId == self.reviewPageId: + self.nextReviewButtonClicked() def addDuplicatesToSongList(self, searchSong, duplicateSong): duplicateGroupFound = False @@ -209,6 +209,23 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ pass + def getNextPageForSearchWizardPage(self): + #if we have not found any duplicates we advance directly to the final page + if len(self.duplicateSongList) == 0: + return self.finalPageId + else: + return self.reviewPageId + + def validateCurrentPage(self): + if self.currentId() == self.reviewPageId: + #as long as the duplicate list is not empty we revisit the review page + if len(self.duplicateSongList) == 0: + return True + else: + self.nextReviewButtonClicked() + return False + return OpenLPWizard.validateCurrentPage(self) + def removeButtonClicked(self, songReviewWidget): #remove song item_id = songReviewWidget.song.id @@ -228,15 +245,54 @@ class DuplicateSongRemovalForm(OpenLPWizard): except OSError: log.exception(u'Could not remove directory: %s', save_path) self.plugin.manager.delete_object(Song, item_id) - #remove GUI elements + # remove GUI elements self.songsHorizontalLayout.removeWidget(songReviewWidget) songReviewWidget.setParent(None) - #check if we only have one SongReviewWidget left + # check if we only have one SongReviewWidget left # 4 stretches + 1 SongReviewWidget = 5 - # the SongReviewWidget is then at position 3 + # the SongReviewWidget is then at position 2 if self.songsHorizontalLayout.count() == 5: self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) + def nextReviewButtonClicked(self): + #show/hide finish/cancel/nextReview buttons + if len(self.duplicateSongList) <= 1: + self.button(QtGui.QWizard.CancelButton).setEnabled(False) + # remove all previous elements + for i in reversed(range(self.songsHorizontalLayout.count())): + item = self.songsHorizontalLayout.itemAt(i) + if isinstance(item, QtGui.QWidgetItem): + # the order is important here, if the .setParent(None) call is done before the .removeItem() call, a + # segfault occurs + widget = item.widget() + self.songsHorizontalLayout.removeItem(item) + widget.setParent(None) + else: + self.songsHorizontalLayout.removeItem(item) + #add next set of duplicates + if len(self.duplicateSongList) > 0: + # a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 + self.songsHorizontalLayout.addStretch() + self.songsHorizontalLayout.addStretch() + for duplicate in self.duplicateSongList.pop(0): + songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) + QtCore.QObject.connect(songReviewWidget, + QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), + self.removeButtonClicked) + self.songsHorizontalLayout.addWidget(songReviewWidget) + self.songsHorizontalLayout.addStretch() + self.songsHorizontalLayout.addStretch() + # add counter + + +class SearchWizardPage(QtGui.QWizardPage): + def __init__(self, parent, nextPageCallback): + QtGui.QWizardPage.__init__(self, parent) + self.nextPageCallback = nextPageCallback + + def nextId(self): + return self.nextPageCallback() + class SongReviewWidget(QtGui.QWidget): def __init__(self, parent, song): QtGui.QWidget.__init__(self, parent) @@ -338,6 +394,7 @@ class SongReviewWidget(QtGui.QWidget): verseMarker = verse[0]['type'] + verse[0]['label'] verseLabel = QtGui.QLabel(self.songInfoVerseGroupBox) verseLabel.setText(verse[1]) + verseLabel.setWordWrap(True) self.songInfoVerseGroupBoxLayout.addRow(verseMarker, verseLabel) self.songContentVerticalLayout.addWidget(self.songInfoVerseGroupBox) self.songContentVerticalLayout.addStretch() diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index b006eb19a..9f69fa700 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -171,24 +171,7 @@ class SongsPlugin(Plugin): """ Search for duplicates in the song database. """ - #maxSongs = self.manager.get_object_count(Song) - #if maxSongs == 0: - # return - #QtGui.QMessageBox.information(self.formParent, - # "Find duplicates called", "Called...") - #songs = self.manager.get_all_objects(Song) - #for outerSongCounter in range(maxSongs-1): - # for innerSongCounter in range(outerSongCounter+1, maxSongs): - # doubleFinder = DuplicateSongFinder() - # if doubleFinder.songsProbablyEqual(songs[outerSongCounter], - # songs[innerSongCounter]): - # QtGui.QMessageBox.information(self.formParent, - # "Double found", str(innerSongCounter) + " " + - # str(outerSongCounter)) - if not hasattr(self, u'duplicate_removal_wizard'): - self.duplicate_removal_wizard = \ - DuplicateSongRemovalForm(self.formParent, self) - self.duplicate_removal_wizard.exec_() + DuplicateSongRemovalForm(self.formParent, self).exec_() def onSongImportItemClicked(self): if self.mediaItem: From b46fac5c829e3c23bfef13c5a06e006775f053b6 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 17 Jan 2013 23:29:37 +0100 Subject: [PATCH 017/235] Add a counter to the song review page. --- .../songs/forms/duplicatesongremovalform.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 93611f83a..57690f742 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -63,10 +63,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``plugin`` The songs plugin. """ - from PyQt4.QtCore import pyqtRemoveInputHook - pyqtRemoveInputHook() - - + self.duplicateSongList = [] + self.reviewCurrentCount = 0 + self.reviewTotalCount = 0 self.clipboard = plugin.formParent.clipboard OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_importsong.bmp', False) @@ -75,7 +74,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Song wizard specific initialisation. """ - self.duplicateSongList = [] + pass def customSignals(self): """ @@ -141,10 +140,14 @@ class DuplicateSongRemovalForm(OpenLPWizard): 'This wizard will help you to remove duplicate songs from the song database.')) self.searchingPage.setTitle(translate('Wizard', 'Searching for doubles')) self.searchingPage.setSubTitle(translate('Wizard', 'The song database is searched for double songs.')) - self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs')) + self.updateReviewCounterText() self.reviewPage.setSubTitle(translate('Wizard', 'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) + def updateReviewCounterText(self): + self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs (%s/%s)') % \ + (self.reviewCurrentCount, self.reviewTotalCount)) + def customPageChanged(self, pageId): """ Called when changing to a page other than the progress page. @@ -167,6 +170,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) + self.reviewTotalCount = len(self.duplicateSongList) elif pageId == self.reviewPageId: self.nextReviewButtonClicked() @@ -255,7 +259,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) def nextReviewButtonClicked(self): - #show/hide finish/cancel/nextReview buttons + self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) + self.updateReviewCounterText() if len(self.duplicateSongList) <= 1: self.button(QtGui.QWizard.CancelButton).setEnabled(False) # remove all previous elements From bc3906fbadee345a8c7af166715b4a8bf6599128 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Fri, 18 Jan 2013 21:08:27 +0100 Subject: [PATCH 018/235] Add a finish page to the wizard. --- .../songs/forms/duplicatesongremovalform.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 57690f742..fd440bf3f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -126,7 +126,19 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) self.reviewPageId = self.addPage(self.reviewPage) self.finalPage = QtGui.QWizardPage() - self.finalPage.setObjectName('finalPage') + self.finalPage.setObjectName(u'finalPage') + self.finalPage.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(u':/wizards/wizard_importsong.bmp')) + self.finalLayout = QtGui.QVBoxLayout(self.finalPage) + self.finalLayout.setObjectName(u'finalLayout') + self.finalTitleLabel = QtGui.QLabel(self.finalPage) + self.finalTitleLabel.setObjectName(u'finalTitleLabel') + self.finalLayout.addWidget(self.finalTitleLabel) + self.finalLayout.addSpacing(40) + self.finalInformationLabel = QtGui.QLabel(self.finalPage) + self.finalInformationLabel.setWordWrap(True) + self.finalInformationLabel.setObjectName(u'finalInformationLabel') + self.finalLayout.addWidget(self.finalInformationLabel) + self.finalLayout.addStretch() self.finalPageId = self.addPage(self.finalPage) def retranslateUi(self): @@ -143,6 +155,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.updateReviewCounterText() self.reviewPage.setSubTitle(translate('Wizard', 'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) + self.finalTitleLabel.setText(WizardStrings.HeaderStyle % translate('Wizard', 'Duplicate Song Removal Wizard sucessfully finished')) + self.finalInformationLabel.setText(translate('Wizard', + 'The Duplicate Song Removal Wizard has finished sucessfully.')) def updateReviewCounterText(self): self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs (%s/%s)') % \ @@ -154,8 +169,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ #hide back button self.button(QtGui.QWizard.BackButton).hide() - if pageId == self.searchingPageId: + #search duplicate songs maxSongs = self.plugin.manager.get_object_count(Song) if maxSongs == 0 or maxSongs == 1: return @@ -171,6 +186,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) self.reviewTotalCount = len(self.duplicateSongList) + if self.reviewTotalCount == 0: + QtGui.QMessageBox.information(self, translate('Wizard', 'Information'), + translate('Wizard', 'No duplicate songs have been found in the database.'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) elif pageId == self.reviewPageId: self.nextReviewButtonClicked() @@ -259,10 +278,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) def nextReviewButtonClicked(self): + # update counter self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) self.updateReviewCounterText() - if len(self.duplicateSongList) <= 1: - self.button(QtGui.QWizard.CancelButton).setEnabled(False) # remove all previous elements for i in reversed(range(self.songsHorizontalLayout.count())): item = self.songsHorizontalLayout.itemAt(i) @@ -287,8 +305,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.addWidget(songReviewWidget) self.songsHorizontalLayout.addStretch() self.songsHorizontalLayout.addStretch() - # add counter - class SearchWizardPage(QtGui.QWizardPage): def __init__(self, parent, nextPageCallback): From 2e7f2392e1b8ebfddd0d8d74df819fe07f5dc0d0 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 21 Jan 2013 21:14:20 +0100 Subject: [PATCH 019/235] Add better image. Remove final page, this simplifies the code in several parts. --- .../songs/forms/duplicatesongremovalform.py | 49 ++++++------------- resources/images/openlp-2.qrc | 1 + 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index fd440bf3f..212ff5eab 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -68,7 +68,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.reviewTotalCount = 0 self.clipboard = plugin.formParent.clipboard OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', - u':/wizards/wizard_importsong.bmp', False) + u':/wizards/wizard_duplicateremoval.bmp', False) def customInit(self): """ @@ -88,7 +88,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): Add song wizard specific pages. """ #add custom pages - self.searchingPage = SearchWizardPage(self, self.getNextPageForSearchWizardPage) + self.searchingPage = QtGui.QWizardPage() self.searchingPage.setObjectName('searchingPage') self.searchingVerticalLayout = QtGui.QVBoxLayout(self.searchingPage) self.searchingVerticalLayout.setObjectName('searchingVerticalLayout') @@ -125,21 +125,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) self.reviewPageId = self.addPage(self.reviewPage) - self.finalPage = QtGui.QWizardPage() - self.finalPage.setObjectName(u'finalPage') - self.finalPage.setPixmap(QtGui.QWizard.WatermarkPixmap, QtGui.QPixmap(u':/wizards/wizard_importsong.bmp')) - self.finalLayout = QtGui.QVBoxLayout(self.finalPage) - self.finalLayout.setObjectName(u'finalLayout') - self.finalTitleLabel = QtGui.QLabel(self.finalPage) - self.finalTitleLabel.setObjectName(u'finalTitleLabel') - self.finalLayout.addWidget(self.finalTitleLabel) - self.finalLayout.addSpacing(40) - self.finalInformationLabel = QtGui.QLabel(self.finalPage) - self.finalInformationLabel.setWordWrap(True) - self.finalInformationLabel.setObjectName(u'finalInformationLabel') - self.finalLayout.addWidget(self.finalInformationLabel) - self.finalLayout.addStretch() - self.finalPageId = self.addPage(self.finalPage) + #add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the + #review page + self.dummyPage = QtGui.QWizardPage() + self.dummyPageId = self.addPage(self.dummyPage) def retranslateUi(self): """ @@ -155,9 +144,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.updateReviewCounterText() self.reviewPage.setSubTitle(translate('Wizard', 'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) - self.finalTitleLabel.setText(WizardStrings.HeaderStyle % translate('Wizard', 'Duplicate Song Removal Wizard sucessfully finished')) - self.finalInformationLabel.setText(translate('Wizard', - 'The Duplicate Song Removal Wizard has finished sucessfully.')) def updateReviewCounterText(self): self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs (%s/%s)') % \ @@ -187,6 +173,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) self.reviewTotalCount = len(self.duplicateSongList) if self.reviewTotalCount == 0: + self.button(QtGui.QWizard.FinishButton).show() + self.button(QtGui.QWizard.FinishButton).setEnabled(True) + self.button(QtGui.QWizard.NextButton).hide() QtGui.QMessageBox.information(self, translate('Wizard', 'Information'), translate('Wizard', 'No duplicate songs have been found in the database.'), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) @@ -232,13 +221,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ pass - def getNextPageForSearchWizardPage(self): - #if we have not found any duplicates we advance directly to the final page - if len(self.duplicateSongList) == 0: - return self.finalPageId - else: - return self.reviewPageId - def validateCurrentPage(self): if self.currentId() == self.reviewPageId: #as long as the duplicate list is not empty we revisit the review page @@ -305,14 +287,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.addWidget(songReviewWidget) self.songsHorizontalLayout.addStretch() self.songsHorizontalLayout.addStretch() - -class SearchWizardPage(QtGui.QWizardPage): - def __init__(self, parent, nextPageCallback): - QtGui.QWizardPage.__init__(self, parent) - self.nextPageCallback = nextPageCallback - - def nextId(self): - return self.nextPageCallback() + #change next button to finish button on last review + if len(self.duplicateSongList) == 0: + self.button(QtGui.QWizard.FinishButton).show() + self.button(QtGui.QWizard.FinishButton).setEnabled(True) + self.button(QtGui.QWizard.NextButton).hide() class SongReviewWidget(QtGui.QWidget): def __init__(self, parent, song): diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index c68bbc4f5..76e9ca8c9 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -99,6 +99,7 @@ wizard_importbible.bmp wizard_firsttime.bmp wizard_createtheme.bmp + wizard_duplicateremoval.bmp service_collapse_all.png From 5f942248340e94047371d958905c705f365b6659 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 21 Jan 2013 21:18:19 +0100 Subject: [PATCH 020/235] Turn all strings into unicode strings. --- .../songs/forms/duplicatesongremovalform.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 212ff5eab..6ba64438f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -89,9 +89,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ #add custom pages self.searchingPage = QtGui.QWizardPage() - self.searchingPage.setObjectName('searchingPage') + self.searchingPage.setObjectName(u'searchingPage') self.searchingVerticalLayout = QtGui.QVBoxLayout(self.searchingPage) - self.searchingVerticalLayout.setObjectName('searchingVerticalLayout') + self.searchingVerticalLayout.setObjectName(u'searchingVerticalLayout') self.duplicateSearchProgressBar = QtGui.QProgressBar(self.searchingPage) self.duplicateSearchProgressBar.setObjectName(u'duplicateSearchProgressBar') self.duplicateSearchProgressBar.setFormat(WizardStrings.PercentSymbolFormat) @@ -99,28 +99,28 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit = QtGui.QPlainTextEdit(self.searchingPage) self.foundDuplicatesEdit.setUndoRedoEnabled(False) self.foundDuplicatesEdit.setReadOnly(True) - self.foundDuplicatesEdit.setObjectName('foundDuplicatesEdit') + self.foundDuplicatesEdit.setObjectName(u'foundDuplicatesEdit') self.searchingVerticalLayout.addWidget(self.foundDuplicatesEdit) self.searchingPageId = self.addPage(self.searchingPage) self.reviewPage = QtGui.QWizardPage() - self.reviewPage.setObjectName('reviewPage') + self.reviewPage.setObjectName(u'reviewPage') self.headerVerticalLayout = QtGui.QVBoxLayout(self.reviewPage) - self.headerVerticalLayout.setObjectName('headerVerticalLayout') + self.headerVerticalLayout.setObjectName(u'headerVerticalLayout') self.reviewCounterLabel = QtGui.QLabel(self.reviewPage) - self.reviewCounterLabel.setObjectName('reviewCounterLabel') + self.reviewCounterLabel.setObjectName(u'reviewCounterLabel') self.headerVerticalLayout.addWidget(self.reviewCounterLabel) self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) - self.songsHorizontalScrollArea.setObjectName('songsHorizontalScrollArea') + self.songsHorizontalScrollArea.setObjectName(u'songsHorizontalScrollArea') self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.songsHorizontalScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) self.songsHorizontalScrollArea.setWidgetResizable(True) - self.songsHorizontalScrollArea.setStyleSheet("QScrollArea#songsHorizontalScrollArea {background-color:transparent;}"); + self.songsHorizontalScrollArea.setStyleSheet(u'QScrollArea#songsHorizontalScrollArea {background-color:transparent;}') self.songsHorizontalSongsWidget = QtGui.QWidget(self.songsHorizontalScrollArea) - self.songsHorizontalSongsWidget.setObjectName('songsHorizontalSongsWidget') - self.songsHorizontalSongsWidget.setStyleSheet("QWidget#songsHorizontalSongsWidget {background-color:transparent;}"); + self.songsHorizontalSongsWidget.setObjectName(u'songsHorizontalSongsWidget') + self.songsHorizontalSongsWidget.setStyleSheet(u'QWidget#songsHorizontalSongsWidget {background-color:transparent;}') self.songsHorizontalLayout = QtGui.QHBoxLayout(self.songsHorizontalSongsWidget) - self.songsHorizontalLayout.setObjectName('songsHorizontalLayout') + self.songsHorizontalLayout.setObjectName(u'songsHorizontalLayout') self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) @@ -134,19 +134,19 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Song wizard localisation. """ - self.setWindowTitle(translate('Wizard', 'Wizard')) - self.titleLabel.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui', - 'Welcome to the Duplicate Song Removal Wizard')) + self.setWindowTitle(translate(u'Wizard', u'Wizard')) + self.titleLabel.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui', + u'Welcome to the Duplicate Song Removal Wizard')) self.informationLabel.setText(translate("Wizard", - 'This wizard will help you to remove duplicate songs from the song database.')) - self.searchingPage.setTitle(translate('Wizard', 'Searching for doubles')) - self.searchingPage.setSubTitle(translate('Wizard', 'The song database is searched for double songs.')) + u'This wizard will help you to remove duplicate songs from the song database.')) + self.searchingPage.setTitle(translate(u'Wizard', u'Searching for doubles')) + self.searchingPage.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) self.updateReviewCounterText() - self.reviewPage.setSubTitle(translate('Wizard', - 'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) + self.reviewPage.setSubTitle(translate(u'Wizard', + u'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) def updateReviewCounterText(self): - self.reviewPage.setTitle(translate('Wizard', 'Review duplicate songs (%s/%s)') % \ + self.reviewPage.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ (self.reviewCurrentCount, self.reviewTotalCount)) def customPageChanged(self, pageId): @@ -176,8 +176,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() - QtGui.QMessageBox.information(self, translate('Wizard', 'Information'), - translate('Wizard', 'No duplicate songs have been found in the database.'), + QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'), + translate(u'Wizard', u'No duplicate songs have been found in the database.'), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) elif pageId == self.reviewPageId: self.nextReviewButtonClicked() @@ -240,11 +240,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): try: os.remove(media_file.file_name) except: - log.exception('Could not remove file: %s', + log.exception(u'Could not remove file: %s', media_file.file_name) try: save_path = os.path.join(AppLocation.get_section_data_path( - self.plugin.name), 'audio', str(item_id)) + self.plugin.name), u'audio', str(item_id)) if os.path.exists(save_path): os.rmdir(save_path) except OSError: From 398b71dda1d51c8ba3ffa8f7fea4290f584ccc46 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 21 Jan 2013 21:27:56 +0100 Subject: [PATCH 021/235] Clarify wizard page descriptions a bit. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 6ba64438f..b6bf29cdf 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -138,12 +138,14 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.titleLabel.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui', u'Welcome to the Duplicate Song Removal Wizard')) self.informationLabel.setText(translate("Wizard", - u'This wizard will help you to remove duplicate songs from the song database.')) - self.searchingPage.setTitle(translate(u'Wizard', u'Searching for doubles')) + u'This wizard will help you to remove duplicate songs from the song database. You will have a chance to ' + u'review every potential duplicate song before it is deleted. So no songs will be deleted without your ' + u'explicit approval.')) + self.searchingPage.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) self.searchingPage.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) self.updateReviewCounterText() self.reviewPage.setSubTitle(translate(u'Wizard', - u'This page shows all duplicate songs to review which ones to remove and which ones to keep.')) + u'Here you can decide which songs to remove and which ones to keep.')) def updateReviewCounterText(self): self.reviewPage.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ From 071fc4799d83ad1db02e156aac965532d46037c0 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 21 Jan 2013 21:30:49 +0100 Subject: [PATCH 022/235] Remove an unused function and a dummy pass statement. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index b6bf29cdf..422c5d871 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -205,7 +205,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): def onWizardExit(self): #refresh the song list self.plugin.mediaItem.onSearchTextButtonClicked() - pass def setDefaults(self): """ @@ -215,14 +214,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicateSearchProgressBar.setValue(0) self.foundDuplicatesEdit.clear() - def performWizard(self): - """ - Perform the actual import. This method pulls in the correct importer - class, and then runs the ``doImport`` method of the importer to do - the actual importing. - """ - pass - def validateCurrentPage(self): if self.currentId() == self.reviewPageId: #as long as the duplicate list is not empty we revisit the review page From 09d99c689bd9e1e1abcd0e103a6bb58de0633c0e Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 21 Jan 2013 21:59:14 +0100 Subject: [PATCH 023/235] Add missing resource image. --- resources/images/wizard_duplicateremoval.bmp | Bin 0 -> 172254 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/images/wizard_duplicateremoval.bmp diff --git a/resources/images/wizard_duplicateremoval.bmp b/resources/images/wizard_duplicateremoval.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ac937714fb4edea8f5d5583b3baa1cdbfaccb675 GIT binary patch literal 172254 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%0AuXenhq!3n*Xj`*4TR%~`2%8=qgpzSS)%F2)3 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 RjRJLD@|m&sER!qPaW_nY&gi%u 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¨9AoxJBAmV>Av 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]pQ}$p6!j(Q*nk_z!pEbA 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 Date: Tue, 22 Jan 2013 21:07:11 +0100 Subject: [PATCH 024/235] Make scroll behavior of review page clearer and a lot simpler. --- .../songs/forms/duplicatesongremovalform.py | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 422c5d871..8e2b74a75 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -112,7 +112,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) self.songsHorizontalScrollArea.setObjectName(u'songsHorizontalScrollArea') self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.songsHorizontalScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) self.songsHorizontalScrollArea.setWidgetResizable(True) self.songsHorizontalScrollArea.setStyleSheet(u'QScrollArea#songsHorizontalScrollArea {background-color:transparent;}') @@ -303,17 +303,6 @@ class SongReviewWidget(QtGui.QWidget): self.songGroupBox.setMaximumWidth(300) self.songGroupBoxLayout = QtGui.QVBoxLayout(self.songGroupBox) self.songGroupBoxLayout.setObjectName(u'songGroupBoxLayout') - self.songScrollArea = QtGui.QScrollArea(self) - self.songScrollArea.setObjectName(u'songScrollArea') - self.songScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.songScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) - self.songScrollArea.setWidgetResizable(True) - self.songContentWidget = QtGui.QWidget(self.songScrollArea) - self.songContentWidget.setObjectName(u'songContentWidget') - self.songContentVerticalLayout = QtGui.QVBoxLayout(self.songContentWidget) - self.songContentVerticalLayout.setObjectName(u'songContentVerticalLayout') - self.songContentVerticalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songInfoFormLayout = QtGui.QFormLayout() self.songInfoFormLayout.setObjectName(u'songInfoFormLayout') self.songTitleLabel = QtGui.QLabel(self) @@ -377,7 +366,7 @@ class SongReviewWidget(QtGui.QWidget): self.songVerseOrderContent.setText(self.song.verse_order) self.songVerseOrderContent.setWordWrap(True) self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) - self.songContentVerticalLayout.addLayout(self.songInfoFormLayout) + self.songGroupBoxLayout.addLayout(self.songInfoFormLayout) self.songInfoVerseGroupBox = QtGui.QGroupBox(self.songGroupBox) self.songInfoVerseGroupBox.setObjectName(u'songInfoVerseGroupBox') self.songInfoVerseGroupBoxLayout = QtGui.QFormLayout(self.songInfoVerseGroupBox) @@ -389,10 +378,8 @@ class SongReviewWidget(QtGui.QWidget): verseLabel.setText(verse[1]) verseLabel.setWordWrap(True) self.songInfoVerseGroupBoxLayout.addRow(verseMarker, verseLabel) - self.songContentVerticalLayout.addWidget(self.songInfoVerseGroupBox) - self.songContentVerticalLayout.addStretch() - self.songScrollArea.setWidget(self.songContentWidget) - self.songGroupBoxLayout.addWidget(self.songScrollArea) + self.songGroupBoxLayout.addWidget(self.songInfoVerseGroupBox) + self.songGroupBoxLayout.addStretch() self.songVerticalLayout.addWidget(self.songGroupBox) self.songRemoveButton = QtGui.QPushButton(self) self.songRemoveButton.setObjectName(u'songRemoveButton') From f179e5ca95336c3991a907de7182a6ac1c8afdd0 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 22 Jan 2013 21:43:46 +0100 Subject: [PATCH 025/235] Lots of comments. --- .../songs/forms/duplicatesongremovalform.py | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 8e2b74a75..ba7bd4c03 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -148,12 +148,15 @@ class DuplicateSongRemovalForm(OpenLPWizard): u'Here you can decide which songs to remove and which ones to keep.')) def updateReviewCounterText(self): + """ + Set the wizard review page header text. + """ self.reviewPage.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ (self.reviewCurrentCount, self.reviewTotalCount)) def customPageChanged(self, pageId): """ - Called when changing to a page other than the progress page. + Called when changing the wizard page. """ #hide back button self.button(QtGui.QWizard.BackButton).hide() @@ -171,7 +174,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): doubleFinder = DuplicateSongFinder() if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) - self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) + self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) self.reviewTotalCount = len(self.duplicateSongList) if self.reviewTotalCount == 0: @@ -185,6 +189,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.nextReviewButtonClicked() def addDuplicatesToSongList(self, searchSong, duplicateSong): + """ + Inserts a song duplicate (two simliar songs) to the duplicate song list. + If one of the two songs is already part of the duplicate song list, + don't add another duplicate group but add the other song to that group. + """ duplicateGroupFound = False for duplicates in self.duplicateSongList: #skip the first song in the duplicate lists, since the first one has to be an earlier song @@ -203,7 +212,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicateSongList.append([searchSong, duplicateSong]) def onWizardExit(self): - #refresh the song list + """ + Once the wizard is finished, refresh the song list, + since we potentially removed songs from it. + """ self.plugin.mediaItem.onSearchTextButtonClicked() def setDefaults(self): @@ -215,6 +227,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.foundDuplicatesEdit.clear() def validateCurrentPage(self): + """ + Controls whether we should switch to the next wizard page. This method loops + on the review page as long as there are more song duplicates to review. + """ if self.currentId() == self.reviewPageId: #as long as the duplicate list is not empty we revisit the review page if len(self.duplicateSongList) == 0: @@ -225,6 +241,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): return OpenLPWizard.validateCurrentPage(self) def removeButtonClicked(self, songReviewWidget): + """ + Removes a song from the database, removes the GUI element representing the + song on the review page, and disable the remove button if only one duplicate + is left. + """ #remove song item_id = songReviewWidget.song.id media_files = self.plugin.manager.get_all_objects(MediaFile, @@ -253,6 +274,13 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) def nextReviewButtonClicked(self): + """ + Called whenever the "next" button is clicked on the review page. + Update the review counter in the wizard header, remove all previous + song widgets, add song widgets for the current duplicate group to review, + if it's the last duplicate song group, hide the "next" button and show + the "finish" button. + """ # update counter self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) self.updateReviewCounterText() @@ -287,6 +315,13 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.NextButton).hide() class SongReviewWidget(QtGui.QWidget): + """ + A widget representing a song on the duplicate song review page. + It displays most of the information a song contains and + provides a "remove" button to remove the song from the database. + The remove logic is not implemented here, but a signal is provided + when the remove button is clicked. + """ def __init__(self, parent, song): QtGui.QWidget.__init__(self, parent) self.song = song @@ -305,6 +340,7 @@ class SongReviewWidget(QtGui.QWidget): self.songGroupBoxLayout.setObjectName(u'songGroupBoxLayout') self.songInfoFormLayout = QtGui.QFormLayout() self.songInfoFormLayout.setObjectName(u'songInfoFormLayout') + #title self.songTitleLabel = QtGui.QLabel(self) self.songTitleLabel.setObjectName(u'songTitleLabel') self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songTitleLabel) @@ -313,6 +349,7 @@ class SongReviewWidget(QtGui.QWidget): self.songTitleContent.setText(self.song.title) self.songTitleContent.setWordWrap(True) self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songTitleContent) + #alternate title self.songAlternateTitleLabel = QtGui.QLabel(self) self.songAlternateTitleLabel.setObjectName(u'songAlternateTitleLabel') self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.songAlternateTitleLabel) @@ -321,6 +358,7 @@ class SongReviewWidget(QtGui.QWidget): self.songAlternateTitleContent.setText(self.song.alternate_title) self.songAlternateTitleContent.setWordWrap(True) self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.songAlternateTitleContent) + #CCLI number self.songCCLINumberLabel = QtGui.QLabel(self) self.songCCLINumberLabel.setObjectName(u'songCCLINumberLabel') self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.songCCLINumberLabel) @@ -329,6 +367,7 @@ class SongReviewWidget(QtGui.QWidget): self.songCCLINumberContent.setText(self.song.ccli_number) self.songCCLINumberContent.setWordWrap(True) self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) + #copyright self.songCopyrightLabel = QtGui.QLabel(self) self.songCopyrightLabel.setObjectName(u'songCopyrightLabel') self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) @@ -337,6 +376,7 @@ class SongReviewWidget(QtGui.QWidget): self.songCopyrightContent.setWordWrap(True) self.songCopyrightContent.setText(self.song.copyright) self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) + #comments self.songCommentsLabel = QtGui.QLabel(self) self.songCommentsLabel.setObjectName(u'songCommentsLabel') self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) @@ -345,6 +385,7 @@ class SongReviewWidget(QtGui.QWidget): self.songCommentsContent.setText(self.song.comments) self.songCommentsContent.setWordWrap(True) self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCommentsContent) + #authors self.songAuthorsLabel = QtGui.QLabel(self) self.songAuthorsLabel.setObjectName(u'songAuthorsLabel') self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) @@ -358,6 +399,7 @@ class SongReviewWidget(QtGui.QWidget): authorsText = authorsText[:-2] self.songAuthorsContent.setText(authorsText) self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) + #verse order self.songVerseOrderLabel = QtGui.QLabel(self) self.songVerseOrderLabel.setObjectName(u'songVerseOrderLabel') self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) @@ -366,6 +408,7 @@ class SongReviewWidget(QtGui.QWidget): self.songVerseOrderContent.setText(self.song.verse_order) self.songVerseOrderContent.setWordWrap(True) self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) + #verses self.songGroupBoxLayout.addLayout(self.songInfoFormLayout) self.songInfoVerseGroupBox = QtGui.QGroupBox(self.songGroupBox) self.songInfoVerseGroupBox.setObjectName(u'songInfoVerseGroupBox') @@ -399,5 +442,8 @@ class SongReviewWidget(QtGui.QWidget): self.songInfoVerseGroupBox.setTitle(u'Verses') def onRemoveButtonClicked(self): + """ + Signal emitted when the "remove" button is clicked. + """ self.emit(QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self) From bbe9293392ceef023671e3c9ee6cbfd98ac6c926 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 22 Jan 2013 21:49:41 +0100 Subject: [PATCH 026/235] Even more comments. --- .../songs/forms/duplicatesongremovalform.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index ba7bd4c03..a6f2538df 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -157,6 +157,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): def customPageChanged(self, pageId): """ Called when changing the wizard page. + + ``pageId`` + ID of the page the wizard changed to. """ #hide back button self.button(QtGui.QWizard.BackButton).hide() @@ -193,6 +196,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): Inserts a song duplicate (two simliar songs) to the duplicate song list. If one of the two songs is already part of the duplicate song list, don't add another duplicate group but add the other song to that group. + + ``searchSong`` + The song we searched the duplicate for. + + ``duplicateSong`` + The duplicate song. """ duplicateGroupFound = False for duplicates in self.duplicateSongList: @@ -245,6 +254,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): Removes a song from the database, removes the GUI element representing the song on the review page, and disable the remove button if only one duplicate is left. + + ``songReviewWidget`` + The SongReviewWidget whose song we should delete. """ #remove song item_id = songReviewWidget.song.id @@ -323,6 +335,13 @@ class SongReviewWidget(QtGui.QWidget): when the remove button is clicked. """ def __init__(self, parent, song): + """ + ``parent`` + The QWidget-derived parent of the wizard. + + ``song`` + The Song which this SongReviewWidget should represent. + """ QtGui.QWidget.__init__(self, parent) self.song = song self.setupUi() From bc3f854921cbbc2e9c0eef2b7dcb3a6b1732e371 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 22 Jan 2013 22:11:01 +0100 Subject: [PATCH 027/235] Lots more comments. --- .../plugins/songs/lib/duplicatesongfinder.py | 104 +++++++++++++----- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py index 6ace0a68b..bf984a5ea 100644 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -26,19 +26,27 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import logging import difflib -from openlp.core.lib import translate from openlp.plugins.songs.lib.db import Song -from openlp.plugins.songs.lib.ui import SongStrings - -log = logging.getLogger(__name__) class DuplicateSongFinder(object): """ The :class:`DuplicateSongFinder` class provides functionality to search for - and remove duplicate songs. + duplicate songs. + + The algorithm is based on the diff algorithm. + First a diffset is calculated for two songs. + To compensate for typos all differences that are smaller than a + limit (minFragmentSize) are removed and the surrounding equal parts are merged. + Finally two conditions can qualify a song tuple to be a duplicate: + 1. There is a block of equal content that is at least minBlockSize large. + This condition should hit for all larger songs that have a long enough + equal part. Even if only one verse is equal this condition should still hit. + 2. Two thirds of the smaller song is contained in the larger song. + This condition should hit if one of the two songs (or both) is small (smaller + than the minBlockSize), but most of the song is contained in the other song. """ def __init__(self): @@ -47,6 +55,15 @@ class DuplicateSongFinder(object): self.maxTypoSize = 3 def songsProbablyEqual(self, song1, song2): + """ + Calculate and return whether two songs are probably equal. + + ``song1`` + The first song to compare. + + ``song2`` + The second song to compare. + """ if len(song1.search_lyrics) < len(song2.search_lyrics): small = song1.search_lyrics large = song2.search_lyrics @@ -64,38 +81,58 @@ class DuplicateSongFinder(object): return False def __opLength(self, opcode): + """ + Return the length of a given difference. + + ``opcode`` + The difference. + """ return max(opcode[2]-opcode[1], opcode[4] - opcode[3]) def __removeTypos(self, diff): + """ + Remove typos from a diff set. A typo is a small difference (minFragmentSize). + + ``diff`` + The diff set to remove the typos from. + """ #remove typo at beginning of string - if len(diff) >= 2: - if diff[0][0] != "equal" and self.__opLength(diff[0]) <= self.maxTypoSize and \ - self.__opLength(diff[1]) >= self.minFragmentSize: - del diff[0] - #remove typos in the middle of string - if len(diff) >= 3: - for index in range(len(diff)-3, -1, -1): - if self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index+1][0] != "equal" and self.__opLength(diff[index+1]) <= self.maxTypoSize and \ - self.__opLength(diff[index+2]) >= self.minFragmentSize: - del diff[index+1] - #remove typo at the end of string - if len(diff) >= 2: - if self.__opLength(diff[-2]) >= self.minFragmentSize and \ - diff[-1][0] != "equal" and self.__opLength(diff[-1]) <= self.maxTypoSize: - del diff[-1] - - #merge fragments - for index in range(len(diff)-2, -1, -1): - if diff[index][0] == "equal" and self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index+1][0] == "equal" and self.__opLength(diff[index+1]) >= self.minFragmentSize: - diff[index] = ("equal", diff[index][1], diff[index+1][2], diff[index][3], - diff[index+1][4]) + if len(diff) >= 2: + if diff[0][0] != "equal" and self.__opLength(diff[0]) <= self.maxTypoSize and \ + self.__opLength(diff[1]) >= self.minFragmentSize: + del diff[0] + #remove typos in the middle of string + if len(diff) >= 3: + for index in range(len(diff)-3, -1, -1): + if self.__opLength(diff[index]) >= self.minFragmentSize and \ + diff[index+1][0] != "equal" and self.__opLength(diff[index+1]) <= self.maxTypoSize and \ + self.__opLength(diff[index+2]) >= self.minFragmentSize: del diff[index+1] + #remove typo at the end of string + if len(diff) >= 2: + if self.__opLength(diff[-2]) >= self.minFragmentSize and \ + diff[-1][0] != "equal" and self.__opLength(diff[-1]) <= self.maxTypoSize: + del diff[-1] - return diff + #merge fragments + for index in range(len(diff)-2, -1, -1): + if diff[index][0] == "equal" and self.__opLength(diff[index]) >= self.minFragmentSize and \ + diff[index+1][0] == "equal" and self.__opLength(diff[index+1]) >= self.minFragmentSize: + diff[index] = ("equal", diff[index][1], diff[index+1][2], diff[index][3], + diff[index+1][4]) + del diff[index+1] + + return diff def __lengthOfEqualBlocks(self, diff): + """ + Return the total length of all equal blocks in a diff set. + Blocks smaller than minBlockSize are not counted. + + ``diff`` + The diff set to return the length for. + """ length = 0 for element in diff: if element[0] == "equal" and self.__opLength(element) >= self.minBlockSize: @@ -103,8 +140,15 @@ class DuplicateSongFinder(object): return length def __lengthOfLongestEqualBlock(self, diff): + """ + Return the length of the largest equal block in a diff set. + + ``diff`` + The diff set to return the length for. + """ length = 0 for element in diff: if element[0] == "equal" and self.__opLength(element) > length: length = self.__opLength(element) return length + From 33c6633dbb51ec1f7df7f25824d578629751c677 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 22 Jan 2013 22:35:53 +0100 Subject: [PATCH 028/235] More documentation. --- openlp/core/ui/wizard.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index be71a0bbe..e79a5d5e0 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -78,6 +78,22 @@ class OpenLPWizard(QtGui.QWizard): """ Generic OpenLP wizard to provide generic functionality and a unified look and feel. + + ``parent`` + The QWidget-derived parent of the wizard. + + ``plugin`` + Plugin this wizard is part of. The plugin will be saved in the "plugin" variable. + The plugin will also be used as basis for the file dialog methods this class provides. + + ``name`` + The object name this wizard should have. + + ``image`` + The image to display on the "welcome" page of the wizard. Should be 163x350. + + ``addProgressPage`` + Whether to add a progress page with a progressbar at the end of the wizard. """ def __init__(self, parent, plugin, name, image, addProgressPage=True): QtGui.QWizard.__init__(self, parent) From d09c8f20a6cc20f54cb3e7555f3ed0056a199c63 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 28 Jan 2013 22:34:38 +0100 Subject: [PATCH 029/235] Remove an unused widget. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index a6f2538df..f393c3fd7 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -104,11 +104,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.searchingPageId = self.addPage(self.searchingPage) self.reviewPage = QtGui.QWizardPage() self.reviewPage.setObjectName(u'reviewPage') - self.headerVerticalLayout = QtGui.QVBoxLayout(self.reviewPage) - self.headerVerticalLayout.setObjectName(u'headerVerticalLayout') - self.reviewCounterLabel = QtGui.QLabel(self.reviewPage) - self.reviewCounterLabel.setObjectName(u'reviewCounterLabel') - self.headerVerticalLayout.addWidget(self.reviewCounterLabel) + self.reviewLayout = QtGui.QVBoxLayout(self.reviewPage) + self.reviewLayout.setObjectName(u'reviewLayout') self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) self.songsHorizontalScrollArea.setObjectName(u'songsHorizontalScrollArea') self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) @@ -123,7 +120,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.setObjectName(u'songsHorizontalLayout') self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) - self.headerVerticalLayout.addWidget(self.songsHorizontalScrollArea) + self.reviewLayout.addWidget(self.songsHorizontalScrollArea) self.reviewPageId = self.addPage(self.reviewPage) #add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the #review page From cdd52c363f25525a8bc587d0deae70623115f2cc Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 28 Jan 2013 23:20:50 +0100 Subject: [PATCH 030/235] Bugfix: Duplicates were reported more than once. --- .../songs/forms/duplicatesongremovalform.py | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index f393c3fd7..923eaaa5e 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -164,6 +164,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): #search duplicate songs maxSongs = self.plugin.manager.get_object_count(Song) if maxSongs == 0 or maxSongs == 1: + self.duplicateSearchProgressBar.setMaximum(1) + self.duplicateSearchProgressBar.setValue(1) + self.notifyNoDuplicates() return # with x songs we have x*(x-1)/2 comparisons maxProgressCount = maxSongs*(maxSongs-1)/2 @@ -173,26 +176,36 @@ class DuplicateSongRemovalForm(OpenLPWizard): for innerSongCounter in range(outerSongCounter+1, maxSongs): doubleFinder = DuplicateSongFinder() if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): - self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) - self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + + duplicateAdded = self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) + if duplicateAdded: + self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) self.reviewTotalCount = len(self.duplicateSongList) if self.reviewTotalCount == 0: - self.button(QtGui.QWizard.FinishButton).show() - self.button(QtGui.QWizard.FinishButton).setEnabled(True) - self.button(QtGui.QWizard.NextButton).hide() - QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'), - translate(u'Wizard', u'No duplicate songs have been found in the database.'), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) + self.notifyNoDuplicates() elif pageId == self.reviewPageId: self.nextReviewButtonClicked() + def notifyNoDuplicates(self): + """ + Notifies the user, that there were no duplicates found in the database. + """ + self.button(QtGui.QWizard.FinishButton).show() + self.button(QtGui.QWizard.FinishButton).setEnabled(True) + self.button(QtGui.QWizard.NextButton).hide() + QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'), + translate(u'Wizard', u'No duplicate songs have been found in the database.'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) + + def addDuplicatesToSongList(self, searchSong, duplicateSong): """ Inserts a song duplicate (two simliar songs) to the duplicate song list. If one of the two songs is already part of the duplicate song list, don't add another duplicate group but add the other song to that group. + Retruns True if at least one of the songs was added, False if both were already + member of a group. ``searchSong`` The song we searched the duplicate for. @@ -201,21 +214,27 @@ class DuplicateSongRemovalForm(OpenLPWizard): The duplicate song. """ duplicateGroupFound = False - for duplicates in self.duplicateSongList: + duplicateAdded = False + for duplicateGroup in self.duplicateSongList: #skip the first song in the duplicate lists, since the first one has to be an earlier song - for duplicate in duplicates[1:]: - if duplicate == searchSong: - duplicates.append(duplicateSong) - duplicateGroupFound = True - break - elif duplicate == duplicateSong: - duplicates.append(searchSong) - duplicateGroupFound = True - break - if duplicateGroupFound: + if searchSong in duplicateGroup and not duplicateSong in duplicateGroup: + duplicateGroup.append(duplicateSong) + duplicateGroupFound = True + duplicateAdded = True + break + elif not searchSong in duplicateGroup and duplicateSong in duplicateGroup: + duplicateGroup.append(searchSong) + duplicateGroupFound = True + duplicateAdded = True + break + elif searchSong in duplicateGroup and duplicateSong in duplicateGroup: + duplicateGroupFound = True + duplicateAdded = False break if not duplicateGroupFound: self.duplicateSongList.append([searchSong, duplicateSong]) + duplicateGroupFound = True + return duplicateAdded def onWizardExit(self): """ From e807c9cb937ac6dec01dc72f3ca43ffdcb13182d Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 12 Feb 2013 21:38:43 +0100 Subject: [PATCH 031/235] Add basic test logic & tune duplicate identification logic a bit. --- .../plugins/songs/lib/duplicatesongfinder.py | 2 +- .../openlp_plugins/songs/__init__.py | 8 +++ .../openlp_plugins/songs/test_lib.py | 72 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tests/functional/openlp_plugins/songs/__init__.py create mode 100644 tests/functional/openlp_plugins/songs/test_lib.py diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py index bf984a5ea..bb8df21fc 100644 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -70,7 +70,7 @@ class DuplicateSongFinder(object): else: small = song2.search_lyrics large = song1.search_lyrics - differ = difflib.SequenceMatcher(a=small, b=large) + differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() diff_no_typos = self.__removeTypos(diff_tuples) #print(diff_no_typos) diff --git a/tests/functional/openlp_plugins/songs/__init__.py b/tests/functional/openlp_plugins/songs/__init__.py new file mode 100644 index 000000000..0157fb2f0 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/__init__.py @@ -0,0 +1,8 @@ +import sip +sip.setapi(u'QDate', 2) +sip.setapi(u'QDateTime', 2) +sip.setapi(u'QString', 2) +sip.setapi(u'QTextStream', 2) +sip.setapi(u'QTime', 2) +sip.setapi(u'QUrl', 2) +sip.setapi(u'QVariant', 2) diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py new file mode 100644 index 000000000..283991345 --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -0,0 +1,72 @@ +# -*- 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 # +############################################################################### + +from unittest import TestCase + +from mock import MagicMock + +from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder + +class TestLib(TestCase): + def duplicate_song_removal_test(self): + full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was + blind but now i see twas grace that taught my heart to fear and grace my fears relieved how precious did that grace + appear the hour i first believed through many dangers toils and snares i have already come tis grace that brought + me safe thus far and grace will lead me home the lord has promised good to me his word my hope secures he will my + shield and portion be as long as life endures yea when this flesh and heart shall fail and mortal life shall cease + i shall possess within the veil a life of joy and peace when weve been here ten thousand years bright shining as + the sun weve no less days to sing gods praise than when weve first begun''' + short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that grace + appear the hour i first believed''' + error_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was + blind but now i see twas grace that taught my heart to fear and grace my fears relieved how precious did that grace + appear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have already come tis grace that brought + me safe thus far and grace will lead me home the lord has promised good to me his word my hope secures he will my + shield andwhen this flcsh and heart shall fail and mortal life shall cease + i shall possess within the veila lifeofjoy and peace when weve been here ten thousand years bright shining as + the sun weve no less days to sing gods praise than when weve first begun''' + different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love that + old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged cross till + my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a crown''' + dsf = DuplicateSongFinder() + song1 = MagicMock() + song2 = MagicMock() + + song1.search_lyrics = full_lyrics + song2.search_lyrics = full_lyrics + assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + song1.search_lyrics = full_lyrics + song2.search_lyrics = short_lyrics + assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + song1.search_lyrics = full_lyrics + song2.search_lyrics = error_lyrics + assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + song1.search_lyrics = full_lyrics + song2.search_lyrics = different_lyrics + assert dsf.songsProbablyEqual(song1, song2) is False, u'The result should be False' From d8cfc4f28a2f8a19ba65279bc966e4fc63a79497 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 12 Feb 2013 22:44:33 +0100 Subject: [PATCH 032/235] Fix wizard after trunk merge and fix a minor display bug. --- openlp/core/lib/settings.py | 1 + .../songs/forms/duplicatesongremovalform.py | 48 +++++++++++-------- openlp/plugins/songs/songsplugin.py | 4 +- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/openlp/core/lib/settings.py b/openlp/core/lib/settings.py index 0591871cc..f02148bc0 100644 --- a/openlp/core/lib/settings.py +++ b/openlp/core/lib/settings.py @@ -200,6 +200,7 @@ class Settings(QtCore.QSettings): u'shortcuts/songImportItem': [], u'shortcuts/themeScreen': [QtGui.QKeySequence(u'T')], u'shortcuts/toolsReindexItem': [], + u'shortcuts/toolsFindDuplicates': [], u'shortcuts/toolsAlertItem': [u'F7'], u'shortcuts/toolsFirstTimeWizard': [], u'shortcuts/toolsOpenDataFolder': [], diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 923eaaa5e..d55966426 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -66,7 +66,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicateSongList = [] self.reviewCurrentCount = 0 self.reviewTotalCount = 0 - self.clipboard = plugin.formParent.clipboard OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) @@ -185,7 +184,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): if self.reviewTotalCount == 0: self.notifyNoDuplicates() elif pageId == self.reviewPageId: - self.nextReviewButtonClicked() + self.processCurrentDuplicateEntry() def notifyNoDuplicates(self): """ @@ -201,10 +200,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): def addDuplicatesToSongList(self, searchSong, duplicateSong): """ - Inserts a song duplicate (two simliar songs) to the duplicate song list. + Inserts a song duplicate (two similar songs) to the duplicate song list. If one of the two songs is already part of the duplicate song list, don't add another duplicate group but add the other song to that group. - Retruns True if at least one of the songs was added, False if both were already + Returns True if at least one of the songs was added, False if both were already member of a group. ``searchSong`` @@ -233,7 +232,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): break if not duplicateGroupFound: self.duplicateSongList.append([searchSong, duplicateSong]) - duplicateGroupFound = True + duplicateAdded = True return duplicateAdded def onWizardExit(self): @@ -257,11 +256,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): on the review page as long as there are more song duplicates to review. """ if self.currentId() == self.reviewPageId: - #as long as the duplicate list is not empty we revisit the review page - if len(self.duplicateSongList) == 0: + #as long as it's not the last duplicate list entry we revisit the review page + if len(self.duplicateSongList) == 1: return True else: - self.nextReviewButtonClicked() + self.proceedToNextReview() return False return OpenLPWizard.validateCurrentPage(self) @@ -301,17 +300,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): if self.songsHorizontalLayout.count() == 5: self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) - def nextReviewButtonClicked(self): + def proceedToNextReview(self): """ - Called whenever the "next" button is clicked on the review page. - Update the review counter in the wizard header, remove all previous - song widgets, add song widgets for the current duplicate group to review, - if it's the last duplicate song group, hide the "next" button and show - the "finish" button. + Removes the previous review UI elements and calls processCurrentDuplicateEntry. """ - # update counter - self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) - self.updateReviewCounterText() + #remove last duplicate group + self.duplicateSongList.pop() # remove all previous elements for i in reversed(range(self.songsHorizontalLayout.count())): item = self.songsHorizontalLayout.itemAt(i) @@ -322,13 +316,25 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.removeItem(item) widget.setParent(None) else: - self.songsHorizontalLayout.removeItem(item) - #add next set of duplicates + self.songsHorizontalLayout.removeItem(item) + #process next set of duplicates + self.processCurrentDuplicateEntry() + + def processCurrentDuplicateEntry(self): + """ + Update the review counter in the wizard header, add song widgets for + the current duplicate group to review, if it's the last + duplicate song group, hide the "next" button and show the "finish" button. + """ + # update counter + self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) + self.updateReviewCounterText() + # add song elements to the UI if len(self.duplicateSongList) > 0: # a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 self.songsHorizontalLayout.addStretch() self.songsHorizontalLayout.addStretch() - for duplicate in self.duplicateSongList.pop(0): + for duplicate in self.duplicateSongList[-1]: songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) QtCore.QObject.connect(songReviewWidget, QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), @@ -337,7 +343,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songsHorizontalLayout.addStretch() self.songsHorizontalLayout.addStretch() #change next button to finish button on last review - if len(self.duplicateSongList) == 0: + if len(self.duplicateSongList) == 1: self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index c4c0aa3e0..f4654c8a2 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -153,7 +153,7 @@ class SongsPlugin(Plugin): visible=False, triggers=self.onToolsReindexItemTriggered) tools_menu.addAction(self.toolsReindexItem) self.toolsFindDuplicates = create_action(tools_menu, u'toolsFindDuplicates', - text=translate('SongsPlugin', 'Find &duplicate songs'), + text=translate('SongsPlugin', 'Find &Duplicate Songs'), statustip=translate('SongsPlugin', 'Find and remove duplicate songs in the song database.'), visible=False, triggers=self.onToolsFindDuplicatesTriggered) @@ -181,7 +181,7 @@ class SongsPlugin(Plugin): """ Search for duplicates in the song database. """ - DuplicateSongRemovalForm(self.formParent, self).exec_() + DuplicateSongRemovalForm(self.main_window, self).exec_() def onSongImportItemClicked(self): if self.mediaItem: From 9597c9f1294b5408e52e168aa4d31b81c585fb9e Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 12 Feb 2013 23:10:13 +0100 Subject: [PATCH 033/235] Minor cosmetic fix. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index d55966426..5fdf56af5 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -273,6 +273,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``songReviewWidget`` The SongReviewWidget whose song we should delete. """ + #remove song from duplicate song list + self.duplicateSongList[-1].remove(songReviewWidget.song) #remove song item_id = songReviewWidget.song.id media_files = self.plugin.manager.get_all_objects(MediaFile, @@ -294,10 +296,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): # remove GUI elements self.songsHorizontalLayout.removeWidget(songReviewWidget) songReviewWidget.setParent(None) - # check if we only have one SongReviewWidget left + # check if we only have one duplicate left # 4 stretches + 1 SongReviewWidget = 5 # the SongReviewWidget is then at position 2 - if self.songsHorizontalLayout.count() == 5: + if len(self.duplicateSongList[-1]) == 1: self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) def proceedToNextReview(self): From 6d5935a0b9c793999c69fc910c245415a8a20044 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 00:00:05 +0100 Subject: [PATCH 034/235] Fix many whitespace issues. --- .../songs/forms/duplicatesongremovalform.py | 10 ++++----- .../plugins/songs/lib/duplicatesongfinder.py | 22 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 5fdf56af5..ff6c85192 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -167,19 +167,19 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicateSearchProgressBar.setValue(1) self.notifyNoDuplicates() return - # with x songs we have x*(x-1)/2 comparisons - maxProgressCount = maxSongs*(maxSongs-1)/2 + # with x songs we have x*(x - 1) / 2 comparisons + maxProgressCount = maxSongs * (maxSongs - 1) / 2 self.duplicateSearchProgressBar.setMaximum(maxProgressCount) songs = self.plugin.manager.get_all_objects(Song) - for outerSongCounter in range(maxSongs-1): - for innerSongCounter in range(outerSongCounter+1, maxSongs): + for outerSongCounter in range(maxSongs - 1): + for innerSongCounter in range(outerSongCounter + 1, maxSongs): doubleFinder = DuplicateSongFinder() if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): duplicateAdded = self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) if duplicateAdded: self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + songs[innerSongCounter].title) - self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value()+1) + self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value() + 1) self.reviewTotalCount = len(self.duplicateSongList) if self.reviewTotalCount == 0: self.notifyNoDuplicates() diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py index bb8df21fc..e497c885f 100644 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -75,7 +75,7 @@ class DuplicateSongFinder(object): diff_no_typos = self.__removeTypos(diff_tuples) #print(diff_no_typos) if self.__lengthOfEqualBlocks(diff_no_typos) >= self.minBlockSize or \ - self.__lengthOfLongestEqualBlock(diff_no_typos) > len(small)*2/3: + self.__lengthOfLongestEqualBlock(diff_no_typos) > len(small) * 2 / 3: return True else: return False @@ -87,7 +87,7 @@ class DuplicateSongFinder(object): ``opcode`` The difference. """ - return max(opcode[2]-opcode[1], opcode[4] - opcode[3]) + return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) def __removeTypos(self, diff): """ @@ -104,11 +104,11 @@ class DuplicateSongFinder(object): del diff[0] #remove typos in the middle of string if len(diff) >= 3: - for index in range(len(diff)-3, -1, -1): + for index in range(len(diff) - 3, -1, -1): if self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index+1][0] != "equal" and self.__opLength(diff[index+1]) <= self.maxTypoSize and \ - self.__opLength(diff[index+2]) >= self.minFragmentSize: - del diff[index+1] + diff[index + 1][0] != "equal" and self.__opLength(diff[index + 1]) <= self.maxTypoSize and \ + self.__opLength(diff[index + 2]) >= self.minFragmentSize: + del diff[index + 1] #remove typo at the end of string if len(diff) >= 2: if self.__opLength(diff[-2]) >= self.minFragmentSize and \ @@ -116,12 +116,12 @@ class DuplicateSongFinder(object): del diff[-1] #merge fragments - for index in range(len(diff)-2, -1, -1): + for index in range(len(diff) - 2, -1, -1): if diff[index][0] == "equal" and self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index+1][0] == "equal" and self.__opLength(diff[index+1]) >= self.minFragmentSize: - diff[index] = ("equal", diff[index][1], diff[index+1][2], diff[index][3], - diff[index+1][4]) - del diff[index+1] + diff[index + 1][0] == "equal" and self.__opLength(diff[index + 1]) >= self.minFragmentSize: + diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], + diff[index + 1][4]) + del diff[index + 1] return diff From bbd491baa84b6c34a88498f1cf7be61fe221331d Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 00:15:21 +0100 Subject: [PATCH 035/235] Make songsProbablyEqual test more standard like. --- .../openlp_plugins/songs/test_lib.py | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 283991345..d98f97773 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -34,7 +34,11 @@ from mock import MagicMock from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder class TestLib(TestCase): - def duplicate_song_removal_test(self): + + def songs_probably_equal_test(self): + """ + Test the DuplicateSongFinder.songsProbablyEqual function. + """ full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how precious did that grace appear the hour i first believed through many dangers toils and snares i have already come tis grace that brought @@ -58,15 +62,42 @@ class TestLib(TestCase): song1 = MagicMock() song2 = MagicMock() + #GIVEN: Two equal songs song1.search_lyrics = full_lyrics song2.search_lyrics = full_lyrics - assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + + #WHEN: We compare those songs for equality + result = dsf.songsProbablyEqual(song1, song2) + + #THEN: The result should be True + assert result is True, u'The result should be True' + + #GIVEN: A song and a short version of the same song song1.search_lyrics = full_lyrics song2.search_lyrics = short_lyrics - assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + + #WHEN: We compare those songs for equality + result = dsf.songsProbablyEqual(song1, song2) + + #THEN: The result should be True + assert result is True, u'The result should be True' + + #GIVEN: A song and the same song with lots of errors song1.search_lyrics = full_lyrics song2.search_lyrics = error_lyrics - assert dsf.songsProbablyEqual(song1, song2) is True, u'The result should be True' + + #WHEN: We compare those songs for equality + result = dsf.songsProbablyEqual(song1, song2) + + #THEN: The result should be True + assert result is True, u'The result should be True' + + #GIVEN: Two different songs song1.search_lyrics = full_lyrics song2.search_lyrics = different_lyrics - assert dsf.songsProbablyEqual(song1, song2) is False, u'The result should be False' + + #WHEN: We compare those songs for equality + result = dsf.songsProbablyEqual(song1, song2) + + #THEN: The result should be False + assert result is False, u'The result should be False' From 2a16a6ebcb0cb17bfac0fd3492327e8ce737cdd2 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 21:57:13 +0100 Subject: [PATCH 036/235] Rename lots of variables and methods. Shorten some too long lines. --- .../songs/forms/duplicatesongremovalform.py | 433 +++++++++--------- .../plugins/songs/lib/duplicatesongfinder.py | 62 +-- .../openlp_plugins/songs/test_lib.py | 43 +- 3 files changed, 268 insertions(+), 270 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index ff6c85192..6a0326c84 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -63,9 +63,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``plugin`` The songs plugin. """ - self.duplicateSongList = [] - self.reviewCurrentCount = 0 - self.reviewTotalCount = 0 + self.duplicate_song_list = [] + self.review_current_count = 0 + self.review_total_count = 0 OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) @@ -87,44 +87,46 @@ class DuplicateSongRemovalForm(OpenLPWizard): Add song wizard specific pages. """ #add custom pages - self.searchingPage = QtGui.QWizardPage() - self.searchingPage.setObjectName(u'searchingPage') - self.searchingVerticalLayout = QtGui.QVBoxLayout(self.searchingPage) - self.searchingVerticalLayout.setObjectName(u'searchingVerticalLayout') - self.duplicateSearchProgressBar = QtGui.QProgressBar(self.searchingPage) - self.duplicateSearchProgressBar.setObjectName(u'duplicateSearchProgressBar') - self.duplicateSearchProgressBar.setFormat(WizardStrings.PercentSymbolFormat) - self.searchingVerticalLayout.addWidget(self.duplicateSearchProgressBar) - self.foundDuplicatesEdit = QtGui.QPlainTextEdit(self.searchingPage) - self.foundDuplicatesEdit.setUndoRedoEnabled(False) - self.foundDuplicatesEdit.setReadOnly(True) - self.foundDuplicatesEdit.setObjectName(u'foundDuplicatesEdit') - self.searchingVerticalLayout.addWidget(self.foundDuplicatesEdit) - self.searchingPageId = self.addPage(self.searchingPage) - self.reviewPage = QtGui.QWizardPage() - self.reviewPage.setObjectName(u'reviewPage') - self.reviewLayout = QtGui.QVBoxLayout(self.reviewPage) - self.reviewLayout.setObjectName(u'reviewLayout') - self.songsHorizontalScrollArea = QtGui.QScrollArea(self.reviewPage) - self.songsHorizontalScrollArea.setObjectName(u'songsHorizontalScrollArea') - self.songsHorizontalScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songsHorizontalScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songsHorizontalScrollArea.setFrameStyle(QtGui.QFrame.NoFrame) - self.songsHorizontalScrollArea.setWidgetResizable(True) - self.songsHorizontalScrollArea.setStyleSheet(u'QScrollArea#songsHorizontalScrollArea {background-color:transparent;}') - self.songsHorizontalSongsWidget = QtGui.QWidget(self.songsHorizontalScrollArea) - self.songsHorizontalSongsWidget.setObjectName(u'songsHorizontalSongsWidget') - self.songsHorizontalSongsWidget.setStyleSheet(u'QWidget#songsHorizontalSongsWidget {background-color:transparent;}') - self.songsHorizontalLayout = QtGui.QHBoxLayout(self.songsHorizontalSongsWidget) - self.songsHorizontalLayout.setObjectName(u'songsHorizontalLayout') - self.songsHorizontalLayout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) - self.songsHorizontalScrollArea.setWidget(self.songsHorizontalSongsWidget) - self.reviewLayout.addWidget(self.songsHorizontalScrollArea) - self.reviewPageId = self.addPage(self.reviewPage) + self.searching_page = QtGui.QWizardPage() + self.searching_page.setObjectName(u'searching_page') + self.searching_vertical_layout = QtGui.QVBoxLayout(self.searching_page) + self.searching_vertical_layout.setObjectName(u'searching_vertical_layout') + self.duplicate_search_progress_bar = QtGui.QProgressBar(self.searching_page) + self.duplicate_search_progress_bar.setObjectName(u'duplicate_search_progress_bar') + self.duplicate_search_progress_bar.setFormat(WizardStrings.PercentSymbolFormat) + self.searching_vertical_layout.addWidget(self.duplicate_search_progress_bar) + self.found_duplicates_edit = QtGui.QPlainTextEdit(self.searching_page) + self.found_duplicates_edit.setUndoRedoEnabled(False) + self.found_duplicates_edit.setReadOnly(True) + self.found_duplicates_edit.setObjectName(u'found_duplicates_edit') + self.searching_vertical_layout.addWidget(self.found_duplicates_edit) + self.searching_page_id = self.addPage(self.searching_page) + self.review_page = QtGui.QWizardPage() + self.review_page.setObjectName(u'review_page') + self.review_layout = QtGui.QVBoxLayout(self.review_page) + self.review_layout.setObjectName(u'review_layout') + self.songs_horizontal_scroll_area = QtGui.QScrollArea(self.review_page) + self.songs_horizontal_scroll_area.setObjectName(u'songs_horizontal_scroll_area') + self.songs_horizontal_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.songs_horizontal_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.songs_horizontal_scroll_area.setFrameStyle(QtGui.QFrame.NoFrame) + self.songs_horizontal_scroll_area.setWidgetResizable(True) + self.songs_horizontal_scroll_area.setStyleSheet( + u'QScrollArea#songs_horizontal_scroll_area {background-color:transparent;}') + self.songs_horizontal_songs_widget = QtGui.QWidget(self.songs_horizontal_scroll_area) + self.songs_horizontal_songs_widget.setObjectName(u'songs_horizontal_songs_widget') + self.songs_horizontal_songs_widget.setStyleSheet( + u'QWidget#songs_horizontal_songs_widget {background-color:transparent;}') + self.songs_horizontal_layout = QtGui.QHBoxLayout(self.songs_horizontal_songs_widget) + self.songs_horizontal_layout.setObjectName(u'songs_horizontal_layout') + self.songs_horizontal_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) + self.songs_horizontal_scroll_area.setWidget(self.songs_horizontal_songs_widget) + self.review_layout.addWidget(self.songs_horizontal_scroll_area) + self.review_page_id = self.addPage(self.review_page) #add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the #review page - self.dummyPage = QtGui.QWizardPage() - self.dummyPageId = self.addPage(self.dummyPage) + self.dummy_page = QtGui.QWizardPage() + self.dummy_page_id = self.addPage(self.dummy_page) def retranslateUi(self): """ @@ -137,53 +139,54 @@ class DuplicateSongRemovalForm(OpenLPWizard): u'This wizard will help you to remove duplicate songs from the song database. You will have a chance to ' u'review every potential duplicate song before it is deleted. So no songs will be deleted without your ' u'explicit approval.')) - self.searchingPage.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) - self.searchingPage.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) + self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) + self.searching_page.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) self.updateReviewCounterText() - self.reviewPage.setSubTitle(translate(u'Wizard', + self.review_page.setSubTitle(translate(u'Wizard', u'Here you can decide which songs to remove and which ones to keep.')) def updateReviewCounterText(self): """ Set the wizard review page header text. """ - self.reviewPage.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ - (self.reviewCurrentCount, self.reviewTotalCount)) + self.review_page.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ + (self.review_current_count, self.review_total_count)) - def customPageChanged(self, pageId): + def customPageChanged(self, page_id): """ Called when changing the wizard page. - ``pageId`` + ``page_id`` ID of the page the wizard changed to. """ #hide back button self.button(QtGui.QWizard.BackButton).hide() - if pageId == self.searchingPageId: + if page_id == self.searching_page_id: #search duplicate songs - maxSongs = self.plugin.manager.get_object_count(Song) - if maxSongs == 0 or maxSongs == 1: - self.duplicateSearchProgressBar.setMaximum(1) - self.duplicateSearchProgressBar.setValue(1) + max_songs = self.plugin.manager.get_object_count(Song) + if max_songs == 0 or max_songs == 1: + self.duplicate_search_progress_bar.setMaximum(1) + self.duplicate_search_progress_bar.setValue(1) self.notifyNoDuplicates() return # with x songs we have x*(x - 1) / 2 comparisons - maxProgressCount = maxSongs * (maxSongs - 1) / 2 - self.duplicateSearchProgressBar.setMaximum(maxProgressCount) + max_progress_count = max_songs * (max_songs - 1) / 2 + self.duplicate_search_progress_bar.setMaximum(max_progress_count) songs = self.plugin.manager.get_all_objects(Song) - for outerSongCounter in range(maxSongs - 1): - for innerSongCounter in range(outerSongCounter + 1, maxSongs): - doubleFinder = DuplicateSongFinder() - if doubleFinder.songsProbablyEqual(songs[outerSongCounter], songs[innerSongCounter]): - duplicateAdded = self.addDuplicatesToSongList(songs[outerSongCounter], songs[innerSongCounter]) - if duplicateAdded: - self.foundDuplicatesEdit.appendPlainText(songs[outerSongCounter].title + " = " + - songs[innerSongCounter].title) - self.duplicateSearchProgressBar.setValue(self.duplicateSearchProgressBar.value() + 1) - self.reviewTotalCount = len(self.duplicateSongList) - if self.reviewTotalCount == 0: + for outer_song_counter in range(max_songs - 1): + for inner_song_counter in range(outer_song_counter + 1, max_songs): + double_finder = DuplicateSongFinder() + if double_finder.songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): + duplicate_added = self.addDuplicatesToSongList(songs[outer_song_counter], + songs[inner_song_counter]) + if duplicate_added: + self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " + + songs[inner_song_counter].title) + self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) + self.review_total_count = len(self.duplicate_song_list) + if self.review_total_count == 0: self.notifyNoDuplicates() - elif pageId == self.reviewPageId: + elif page_id == self.review_page_id: self.processCurrentDuplicateEntry() def notifyNoDuplicates(self): @@ -198,7 +201,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) - def addDuplicatesToSongList(self, searchSong, duplicateSong): + def addDuplicatesToSongList(self, search_song, duplicate_song): """ Inserts a song duplicate (two similar songs) to the duplicate song list. If one of the two songs is already part of the duplicate song list, @@ -206,34 +209,34 @@ class DuplicateSongRemovalForm(OpenLPWizard): Returns True if at least one of the songs was added, False if both were already member of a group. - ``searchSong`` + ``search_song`` The song we searched the duplicate for. - ``duplicateSong`` + ``duplicate_song`` The duplicate song. """ - duplicateGroupFound = False - duplicateAdded = False - for duplicateGroup in self.duplicateSongList: + duplicate_group_found = False + duplicate_added = False + for duplicate_group in self.duplicate_song_list: #skip the first song in the duplicate lists, since the first one has to be an earlier song - if searchSong in duplicateGroup and not duplicateSong in duplicateGroup: - duplicateGroup.append(duplicateSong) - duplicateGroupFound = True - duplicateAdded = True + if search_song in duplicate_group and not duplicate_song in duplicate_group: + duplicate_group.append(duplicate_song) + duplicate_group_found = True + duplicate_added = True break - elif not searchSong in duplicateGroup and duplicateSong in duplicateGroup: - duplicateGroup.append(searchSong) - duplicateGroupFound = True - duplicateAdded = True + elif not search_song in duplicate_group and duplicate_song in duplicate_group: + duplicate_group.append(search_song) + duplicate_group_found = True + duplicate_added = True break - elif searchSong in duplicateGroup and duplicateSong in duplicateGroup: - duplicateGroupFound = True - duplicateAdded = False + elif search_song in duplicate_group and duplicate_song in duplicate_group: + duplicate_group_found = True + duplicate_added = False break - if not duplicateGroupFound: - self.duplicateSongList.append([searchSong, duplicateSong]) - duplicateAdded = True - return duplicateAdded + if not duplicate_group_found: + self.duplicate_song_list.append([search_song, duplicate_song]) + duplicate_added = True + return duplicate_added def onWizardExit(self): """ @@ -247,36 +250,36 @@ class DuplicateSongRemovalForm(OpenLPWizard): Set default form values for the song import wizard. """ self.restart() - self.duplicateSearchProgressBar.setValue(0) - self.foundDuplicatesEdit.clear() + self.duplicate_search_progress_bar.setValue(0) + self.found_duplicates_edit.clear() def validateCurrentPage(self): """ Controls whether we should switch to the next wizard page. This method loops on the review page as long as there are more song duplicates to review. """ - if self.currentId() == self.reviewPageId: + if self.currentId() == self.review_page_id: #as long as it's not the last duplicate list entry we revisit the review page - if len(self.duplicateSongList) == 1: + if len(self.duplicate_song_list) == 1: return True else: self.proceedToNextReview() return False return OpenLPWizard.validateCurrentPage(self) - def removeButtonClicked(self, songReviewWidget): + def removeButtonClicked(self, song_review_widget): """ Removes a song from the database, removes the GUI element representing the song on the review page, and disable the remove button if only one duplicate is left. - ``songReviewWidget`` + ``song_review_widget`` The SongReviewWidget whose song we should delete. """ #remove song from duplicate song list - self.duplicateSongList[-1].remove(songReviewWidget.song) + self.duplicate_song_list[-1].remove(song_review_widget.song) #remove song - item_id = songReviewWidget.song.id + item_id = song_review_widget.song.id media_files = self.plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == item_id) for media_file in media_files: @@ -294,31 +297,31 @@ class DuplicateSongRemovalForm(OpenLPWizard): log.exception(u'Could not remove directory: %s', save_path) self.plugin.manager.delete_object(Song, item_id) # remove GUI elements - self.songsHorizontalLayout.removeWidget(songReviewWidget) - songReviewWidget.setParent(None) + self.songs_horizontal_layout.removeWidget(song_review_widget) + song_review_widget.setParent(None) # check if we only have one duplicate left # 4 stretches + 1 SongReviewWidget = 5 # the SongReviewWidget is then at position 2 - if len(self.duplicateSongList[-1]) == 1: - self.songsHorizontalLayout.itemAt(2).widget().songRemoveButton.setEnabled(False) + if len(self.duplicate_song_list[-1]) == 1: + self.songs_horizontal_layout.itemAt(2).widget().song_remove_button.setEnabled(False) def proceedToNextReview(self): """ Removes the previous review UI elements and calls processCurrentDuplicateEntry. """ #remove last duplicate group - self.duplicateSongList.pop() + self.duplicate_song_list.pop() # remove all previous elements - for i in reversed(range(self.songsHorizontalLayout.count())): - item = self.songsHorizontalLayout.itemAt(i) + for i in reversed(range(self.songs_horizontal_layout.count())): + item = self.songs_horizontal_layout.itemAt(i) if isinstance(item, QtGui.QWidgetItem): - # the order is important here, if the .setParent(None) call is done before the .removeItem() call, a - # segfault occurs + # The order is important here, if the .setParent(None) call is done + # before the .removeItem() call, a segfault occurs. widget = item.widget() - self.songsHorizontalLayout.removeItem(item) + self.songs_horizontal_layout.removeItem(item) widget.setParent(None) else: - self.songsHorizontalLayout.removeItem(item) + self.songs_horizontal_layout.removeItem(item) #process next set of duplicates self.processCurrentDuplicateEntry() @@ -329,23 +332,23 @@ class DuplicateSongRemovalForm(OpenLPWizard): duplicate song group, hide the "next" button and show the "finish" button. """ # update counter - self.reviewCurrentCount = self.reviewTotalCount - (len(self.duplicateSongList) - 1) + self.review_current_count = self.review_total_count - (len(self.duplicate_song_list) - 1) self.updateReviewCounterText() # add song elements to the UI - if len(self.duplicateSongList) > 0: + if len(self.duplicate_song_list) > 0: # a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 - self.songsHorizontalLayout.addStretch() - self.songsHorizontalLayout.addStretch() - for duplicate in self.duplicateSongList[-1]: - songReviewWidget = SongReviewWidget(self.reviewPage, duplicate) - QtCore.QObject.connect(songReviewWidget, + self.songs_horizontal_layout.addStretch() + self.songs_horizontal_layout.addStretch() + for duplicate in self.duplicate_song_list[-1]: + song_review_widget = SongReviewWidget(self.review_page, duplicate) + QtCore.QObject.connect(song_review_widget, QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self.removeButtonClicked) - self.songsHorizontalLayout.addWidget(songReviewWidget) - self.songsHorizontalLayout.addStretch() - self.songsHorizontalLayout.addStretch() + self.songs_horizontal_layout.addWidget(song_review_widget) + self.songs_horizontal_layout.addStretch() + self.songs_horizontal_layout.addStretch() #change next button to finish button on last review - if len(self.duplicateSongList) == 1: + if len(self.duplicate_song_list) == 1: self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() @@ -370,119 +373,119 @@ class SongReviewWidget(QtGui.QWidget): self.song = song self.setupUi() self.retranslateUi() - QtCore.QObject.connect(self.songRemoveButton, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) + QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) def setupUi(self): - self.songVerticalLayout = QtGui.QVBoxLayout(self) - self.songVerticalLayout.setObjectName(u'songVerticalLayout') - self.songGroupBox = QtGui.QGroupBox(self) - self.songGroupBox.setObjectName(u'songGroupBox') - self.songGroupBox.setMinimumWidth(300) - self.songGroupBox.setMaximumWidth(300) - self.songGroupBoxLayout = QtGui.QVBoxLayout(self.songGroupBox) - self.songGroupBoxLayout.setObjectName(u'songGroupBoxLayout') - self.songInfoFormLayout = QtGui.QFormLayout() - self.songInfoFormLayout.setObjectName(u'songInfoFormLayout') + self.song_vertical_layout = QtGui.QVBoxLayout(self) + self.song_vertical_layout.setObjectName(u'song_vertical_layout') + self.song_group_box = QtGui.QGroupBox(self) + self.song_group_box.setObjectName(u'song_group_box') + self.song_group_box.setMinimumWidth(300) + self.song_group_box.setMaximumWidth(300) + self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) + self.song_group_box_layout.setObjectName(u'song_group_box_layout') + self.song_info_form_layout = QtGui.QFormLayout() + self.song_info_form_layout.setObjectName(u'song_info_form_layout') #title - self.songTitleLabel = QtGui.QLabel(self) - self.songTitleLabel.setObjectName(u'songTitleLabel') - self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.LabelRole, self.songTitleLabel) - self.songTitleContent = QtGui.QLabel(self) - self.songTitleContent.setObjectName(u'songTitleContent') - self.songTitleContent.setText(self.song.title) - self.songTitleContent.setWordWrap(True) - self.songInfoFormLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.songTitleContent) + self.song_title_label = QtGui.QLabel(self) + self.song_title_label.setObjectName(u'song_title_label') + self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label) + self.song_title_content = QtGui.QLabel(self) + self.song_title_content.setObjectName(u'song_title_content') + self.song_title_content.setText(self.song.title) + self.song_title_content.setWordWrap(True) + self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content) #alternate title - self.songAlternateTitleLabel = QtGui.QLabel(self) - self.songAlternateTitleLabel.setObjectName(u'songAlternateTitleLabel') - self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.LabelRole, self.songAlternateTitleLabel) - self.songAlternateTitleContent = QtGui.QLabel(self) - self.songAlternateTitleContent.setObjectName(u'songAlternateTitleContent') - self.songAlternateTitleContent.setText(self.song.alternate_title) - self.songAlternateTitleContent.setWordWrap(True) - self.songInfoFormLayout.setWidget(1, QtGui.QFormLayout.FieldRole, self.songAlternateTitleContent) + self.song_alternate_title_label = QtGui.QLabel(self) + self.song_alternate_title_label.setObjectName(u'song_alternate_title_label') + self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label) + self.song_alternate_title_content = QtGui.QLabel(self) + self.song_alternate_title_content.setObjectName(u'song_alternate_title_content') + self.song_alternate_title_content.setText(self.song.alternate_title) + self.song_alternate_title_content.setWordWrap(True) + self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content) #CCLI number - self.songCCLINumberLabel = QtGui.QLabel(self) - self.songCCLINumberLabel.setObjectName(u'songCCLINumberLabel') - self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.LabelRole, self.songCCLINumberLabel) - self.songCCLINumberContent = QtGui.QLabel(self) - self.songCCLINumberContent.setObjectName(u'songCCLINumberContent') - self.songCCLINumberContent.setText(self.song.ccli_number) - self.songCCLINumberContent.setWordWrap(True) - self.songInfoFormLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.songCCLINumberContent) + self.song_ccli_number_label = QtGui.QLabel(self) + self.song_ccli_number_label.setObjectName(u'song_ccli_number_label') + self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label) + self.song_ccli_number_content = QtGui.QLabel(self) + self.song_ccli_number_content.setObjectName(u'song_ccli_number_content') + self.song_ccli_number_content.setText(self.song.ccli_number) + self.song_ccli_number_content.setWordWrap(True) + self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content) #copyright - self.songCopyrightLabel = QtGui.QLabel(self) - self.songCopyrightLabel.setObjectName(u'songCopyrightLabel') - self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.songCopyrightLabel) - self.songCopyrightContent = QtGui.QLabel(self) - self.songCopyrightContent.setObjectName(u'songCopyrightContent') - self.songCopyrightContent.setWordWrap(True) - self.songCopyrightContent.setText(self.song.copyright) - self.songInfoFormLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.songCopyrightContent) + self.song_copyright_label = QtGui.QLabel(self) + self.song_copyright_label.setObjectName(u'song_copyright_label') + self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label) + self.song_copyright_content = QtGui.QLabel(self) + self.song_copyright_content.setObjectName(u'song_copyright_content') + self.song_copyright_content.setWordWrap(True) + self.song_copyright_content.setText(self.song.copyright) + self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content) #comments - self.songCommentsLabel = QtGui.QLabel(self) - self.songCommentsLabel.setObjectName(u'songCommentsLabel') - self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.songCommentsLabel) - self.songCommentsContent = QtGui.QLabel(self) - self.songCommentsContent.setObjectName(u'songCommentsContent') - self.songCommentsContent.setText(self.song.comments) - self.songCommentsContent.setWordWrap(True) - self.songInfoFormLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.songCommentsContent) + self.song_comments_label = QtGui.QLabel(self) + self.song_comments_label.setObjectName(u'song_comments_label') + self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label) + self.song_comments_content = QtGui.QLabel(self) + self.song_comments_content.setObjectName(u'song_comments_content') + self.song_comments_content.setText(self.song.comments) + self.song_comments_content.setWordWrap(True) + self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content) #authors - self.songAuthorsLabel = QtGui.QLabel(self) - self.songAuthorsLabel.setObjectName(u'songAuthorsLabel') - self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.songAuthorsLabel) - self.songAuthorsContent = QtGui.QLabel(self) - self.songAuthorsContent.setObjectName(u'songAuthorsContent') - self.songAuthorsContent.setWordWrap(True) - authorsText = u'' + self.song_authors_label = QtGui.QLabel(self) + self.song_authors_label.setObjectName(u'song_authors_label') + self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label) + self.song_authors_content = QtGui.QLabel(self) + self.song_authors_content.setObjectName(u'song_authors_content') + self.song_authors_content.setWordWrap(True) + authors_text = u'' for author in self.song.authors: - authorsText += author.display_name + ', ' - if authorsText: - authorsText = authorsText[:-2] - self.songAuthorsContent.setText(authorsText) - self.songInfoFormLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.songAuthorsContent) + authors_text += author.display_name + ', ' + if authors_text: + authors_text = authors_text[:-2] + self.song_authors_content.setText(authors_text) + self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) #verse order - self.songVerseOrderLabel = QtGui.QLabel(self) - self.songVerseOrderLabel.setObjectName(u'songVerseOrderLabel') - self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.LabelRole, self.songVerseOrderLabel) - self.songVerseOrderContent = QtGui.QLabel(self) - self.songVerseOrderContent.setObjectName(u'songVerseOrderContent') - self.songVerseOrderContent.setText(self.song.verse_order) - self.songVerseOrderContent.setWordWrap(True) - self.songInfoFormLayout.setWidget(6, QtGui.QFormLayout.FieldRole, self.songVerseOrderContent) + self.song_verse_order_label = QtGui.QLabel(self) + self.song_verse_order_label.setObjectName(u'song_verse_order_label') + self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label) + self.song_verse_order_content = QtGui.QLabel(self) + self.song_verse_order_content.setObjectName(u'song_verse_order_content') + self.song_verse_order_content.setText(self.song.verse_order) + self.song_verse_order_content.setWordWrap(True) + self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) #verses - self.songGroupBoxLayout.addLayout(self.songInfoFormLayout) - self.songInfoVerseGroupBox = QtGui.QGroupBox(self.songGroupBox) - self.songInfoVerseGroupBox.setObjectName(u'songInfoVerseGroupBox') - self.songInfoVerseGroupBoxLayout = QtGui.QFormLayout(self.songInfoVerseGroupBox) - songXml = SongXML() - verses = songXml.get_verses(self.song.lyrics) + self.song_group_box_layout.addLayout(self.song_info_form_layout) + self.song_info_verse_group_box = QtGui.QGroupBox(self.song_group_box) + self.song_info_verse_group_box.setObjectName(u'song_info_verse_group_box') + self.song_info_verse_group_box_layout = QtGui.QFormLayout(self.song_info_verse_group_box) + song_xml = SongXML() + verses = song_xml.get_verses(self.song.lyrics) for verse in verses: - verseMarker = verse[0]['type'] + verse[0]['label'] - verseLabel = QtGui.QLabel(self.songInfoVerseGroupBox) - verseLabel.setText(verse[1]) - verseLabel.setWordWrap(True) - self.songInfoVerseGroupBoxLayout.addRow(verseMarker, verseLabel) - self.songGroupBoxLayout.addWidget(self.songInfoVerseGroupBox) - self.songGroupBoxLayout.addStretch() - self.songVerticalLayout.addWidget(self.songGroupBox) - self.songRemoveButton = QtGui.QPushButton(self) - self.songRemoveButton.setObjectName(u'songRemoveButton') - self.songRemoveButton.setIcon(build_icon(u':/songs/song_delete.png')) - self.songRemoveButton.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - self.songVerticalLayout.addWidget(self.songRemoveButton, alignment = QtCore.Qt.AlignHCenter) + verse_marker = verse[0]['type'] + verse[0]['label'] + verse_label = QtGui.QLabel(self.song_info_verse_group_box) + verse_label.setText(verse[1]) + verse_label.setWordWrap(True) + self.song_info_verse_group_box_layout.addRow(verse_marker, verse_label) + self.song_group_box_layout.addWidget(self.song_info_verse_group_box) + self.song_group_box_layout.addStretch() + self.song_vertical_layout.addWidget(self.song_group_box) + self.song_remove_button = QtGui.QPushButton(self) + self.song_remove_button.setObjectName(u'song_remove_button') + self.song_remove_button.setIcon(build_icon(u':/songs/song_delete.png')) + self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.song_vertical_layout.addWidget(self.song_remove_button, alignment = QtCore.Qt.AlignHCenter) def retranslateUi(self): - self.songRemoveButton.setText(u'Remove') - self.songTitleLabel.setText(u'Title:') - self.songAlternateTitleLabel.setText(u'Alternate Title:') - self.songCCLINumberLabel.setText(u'CCLI Number:') - self.songVerseOrderLabel.setText(u'Verse Order:') - self.songCopyrightLabel.setText(u'Copyright:') - self.songCommentsLabel.setText(u'Comments:') - self.songAuthorsLabel.setText(u'Authors:') - self.songInfoVerseGroupBox.setTitle(u'Verses') + self.song_remove_button.setText(u'Remove') + self.song_title_label.setText(u'Title:') + self.song_alternate_title_label.setText(u'Alternate Title:') + self.song_ccli_number_label.setText(u'CCLI Number:') + self.song_verse_order_label.setText(u'Verse Order:') + self.song_copyright_label.setText(u'Copyright:') + self.song_comments_label.setText(u'Comments:') + self.song_authors_label.setText(u'Authors:') + self.song_info_verse_group_box.setTitle(u'Verses') def onRemoveButtonClicked(self): """ diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py index e497c885f..38062632d 100644 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -38,23 +38,23 @@ class DuplicateSongFinder(object): The algorithm is based on the diff algorithm. First a diffset is calculated for two songs. To compensate for typos all differences that are smaller than a - limit (minFragmentSize) are removed and the surrounding equal parts are merged. + limit (min_fragment_size) are removed and the surrounding equal parts are merged. Finally two conditions can qualify a song tuple to be a duplicate: - 1. There is a block of equal content that is at least minBlockSize large. + 1. There is a block of equal content that is at least min_block_size large. This condition should hit for all larger songs that have a long enough equal part. Even if only one verse is equal this condition should still hit. 2. Two thirds of the smaller song is contained in the larger song. This condition should hit if one of the two songs (or both) is small (smaller - than the minBlockSize), but most of the song is contained in the other song. + than the min_block_size), but most of the song is contained in the other song. """ def __init__(self): - self.minFragmentSize = 5 - self.minBlockSize = 70 - self.maxTypoSize = 3 + self.min_fragment_size = 5 + self.min_block_size = 70 + self.max_typo_size = 3 - def songsProbablyEqual(self, song1, song2): + def songs_probably_equal(self, song1, song2): """ Calculate and return whether two songs are probably equal. @@ -72,15 +72,15 @@ class DuplicateSongFinder(object): large = song1.search_lyrics differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() - diff_no_typos = self.__removeTypos(diff_tuples) + diff_no_typos = self.__remove_typos(diff_tuples) #print(diff_no_typos) - if self.__lengthOfEqualBlocks(diff_no_typos) >= self.minBlockSize or \ - self.__lengthOfLongestEqualBlock(diff_no_typos) > len(small) * 2 / 3: + if self.__length_of_equal_blocks(diff_no_typos) >= self.min_block_size or \ + self.__length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: return True else: return False - def __opLength(self, opcode): + def __op_length(self, opcode): """ Return the length of a given difference. @@ -89,57 +89,57 @@ class DuplicateSongFinder(object): """ return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) - def __removeTypos(self, diff): + def __remove_typos(self, diff): """ - Remove typos from a diff set. A typo is a small difference (minFragmentSize). + Remove typos from a diff set. A typo is a small difference (min_fragment_size). ``diff`` The diff set to remove the typos from. """ #remove typo at beginning of string if len(diff) >= 2: - if diff[0][0] != "equal" and self.__opLength(diff[0]) <= self.maxTypoSize and \ - self.__opLength(diff[1]) >= self.minFragmentSize: + if diff[0][0] != "equal" and self.__op_length(diff[0]) <= self.max_typo_size and \ + self.__op_length(diff[1]) >= self.min_fragment_size: del diff[0] #remove typos in the middle of string if len(diff) >= 3: for index in range(len(diff) - 3, -1, -1): - if self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index + 1][0] != "equal" and self.__opLength(diff[index + 1]) <= self.maxTypoSize and \ - self.__opLength(diff[index + 2]) >= self.minFragmentSize: + if self.__op_length(diff[index]) >= self.min_fragment_size and \ + diff[index + 1][0] != "equal" and self.__op_length(diff[index + 1]) <= self.max_typo_size and \ + self.__op_length(diff[index + 2]) >= self.min_fragment_size: del diff[index + 1] #remove typo at the end of string if len(diff) >= 2: - if self.__opLength(diff[-2]) >= self.minFragmentSize and \ - diff[-1][0] != "equal" and self.__opLength(diff[-1]) <= self.maxTypoSize: + if self.__op_length(diff[-2]) >= self.min_fragment_size and \ + diff[-1][0] != "equal" and self.__op_length(diff[-1]) <= self.max_typo_size: del diff[-1] #merge fragments for index in range(len(diff) - 2, -1, -1): - if diff[index][0] == "equal" and self.__opLength(diff[index]) >= self.minFragmentSize and \ - diff[index + 1][0] == "equal" and self.__opLength(diff[index + 1]) >= self.minFragmentSize: + if diff[index][0] == "equal" and self.__op_length(diff[index]) >= self.min_fragment_size and \ + diff[index + 1][0] == "equal" and self.__op_length(diff[index + 1]) >= self.min_fragment_size: diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], diff[index + 1][4]) del diff[index + 1] return diff - def __lengthOfEqualBlocks(self, diff): + def __length_of_equal_blocks(self, diff): """ Return the total length of all equal blocks in a diff set. - Blocks smaller than minBlockSize are not counted. + Blocks smaller than min_block_size are not counted. ``diff`` The diff set to return the length for. """ length = 0 for element in diff: - if element[0] == "equal" and self.__opLength(element) >= self.minBlockSize: - length += self.__opLength(element) + if element[0] == "equal" and self.__op_length(element) >= self.min_block_size: + length += self.__op_length(element) return length - def __lengthOfLongestEqualBlock(self, diff): + def __length_of_longest_equal_block(self, diff): """ Return the length of the largest equal block in a diff set. @@ -148,7 +148,7 @@ class DuplicateSongFinder(object): """ length = 0 for element in diff: - if element[0] == "equal" and self.__opLength(element) > length: - length = self.__opLength(element) + if element[0] == "equal" and self.__op_length(element) > length: + length = self.__op_length(element) return length diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index d98f97773..6de41da37 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -37,27 +37,22 @@ class TestLib(TestCase): def songs_probably_equal_test(self): """ - Test the DuplicateSongFinder.songsProbablyEqual function. + Test the DuplicateSongFinder.songs_probably_equal function. """ - full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was - blind but now i see twas grace that taught my heart to fear and grace my fears relieved how precious did that grace - appear the hour i first believed through many dangers toils and snares i have already come tis grace that brought - me safe thus far and grace will lead me home the lord has promised good to me his word my hope secures he will my - shield and portion be as long as life endures yea when this flesh and heart shall fail and mortal life shall cease - i shall possess within the veil a life of joy and peace when weve been here ten thousand years bright shining as - the sun weve no less days to sing gods praise than when weve first begun''' - short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that grace - appear the hour i first believed''' - error_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was - blind but now i see twas grace that taught my heart to fear and grace my fears relieved how precious did that grace - appear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have already come tis grace that brought - me safe thus far and grace will lead me home the lord has promised good to me his word my hope secures he will my - shield andwhen this flcsh and heart shall fail and mortal life shall cease - i shall possess within the veila lifeofjoy and peace when weve been here ten thousand years bright shining as - the sun weve no less days to sing gods praise than when weve first begun''' - different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love that - old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged cross till - my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a crown''' + full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am + found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how + precious did that grace appear the hour i first believed through many dangers toils and snares i have already + come tis grace that brought me safe thus far and grace will lead me home''' + short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that + grace appear the hour i first believed''' + error_lyrics =u'''amazing how sweet the trumpet that saved a wrench like me i once was losst but now am + found waf blind but now i see it was grace that taught my heart to fear and grace my fears relieved how + precious did that grace appppppppear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have + already come to this grace that brought me safe so far and grace will lead me home''' + different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love + that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged + cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a + crown''' dsf = DuplicateSongFinder() song1 = MagicMock() song2 = MagicMock() @@ -67,7 +62,7 @@ class TestLib(TestCase): song2.search_lyrics = full_lyrics #WHEN: We compare those songs for equality - result = dsf.songsProbablyEqual(song1, song2) + result = dsf.songs_probably_equal(song1, song2) #THEN: The result should be True assert result is True, u'The result should be True' @@ -77,7 +72,7 @@ class TestLib(TestCase): song2.search_lyrics = short_lyrics #WHEN: We compare those songs for equality - result = dsf.songsProbablyEqual(song1, song2) + result = dsf.songs_probably_equal(song1, song2) #THEN: The result should be True assert result is True, u'The result should be True' @@ -87,7 +82,7 @@ class TestLib(TestCase): song2.search_lyrics = error_lyrics #WHEN: We compare those songs for equality - result = dsf.songsProbablyEqual(song1, song2) + result = dsf.songs_probably_equal(song1, song2) #THEN: The result should be True assert result is True, u'The result should be True' @@ -97,7 +92,7 @@ class TestLib(TestCase): song2.search_lyrics = different_lyrics #WHEN: We compare those songs for equality - result = dsf.songsProbablyEqual(song1, song2) + result = dsf.songs_probably_equal(song1, song2) #THEN: The result should be False assert result is False, u'The result should be False' From f7c19f599c77eadeaab85f42db250cd07d55c4e6 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 22:01:10 +0100 Subject: [PATCH 037/235] Move customInit to wizard. So dummy implementations are not required in the deriving classes. --- openlp/core/ui/wizard.py | 6 ++++++ openlp/plugins/songs/forms/duplicatesongremovalform.py | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 001bfbf88..b06c140b6 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -206,6 +206,12 @@ class OpenLPWizard(QtGui.QWizard): else: self.customPageChanged(pageId) + def customInit(self): + """ + Song wizard specific initialization. + """ + pass + def customPageChanged(self, pageId): """ Called when changing to a page other than the progress page diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 6a0326c84..83336d288 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -69,12 +69,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) - def customInit(self): - """ - Song wizard specific initialisation. - """ - pass - def customSignals(self): """ Song wizard specific signals. From 89b9a1dabc3f14e3e280300f58b91e3bfb1375d5 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 22:17:24 +0100 Subject: [PATCH 038/235] Move SongReviewWidget into separate file. --- .../songs/forms/duplicatesongremovalform.py | 143 +------------- .../plugins/songs/forms/songreviewwidget.py | 175 ++++++++++++++++++ 2 files changed, 176 insertions(+), 142 deletions(-) create mode 100644 openlp/plugins/songs/forms/songreviewwidget.py diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 83336d288..4e43e94d2 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -41,8 +41,8 @@ from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.core.utils import AppLocation from openlp.plugins.songs.lib.db import Song, MediaFile -from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder +from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget log = logging.getLogger(__name__) @@ -346,144 +346,3 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() - -class SongReviewWidget(QtGui.QWidget): - """ - A widget representing a song on the duplicate song review page. - It displays most of the information a song contains and - provides a "remove" button to remove the song from the database. - The remove logic is not implemented here, but a signal is provided - when the remove button is clicked. - """ - def __init__(self, parent, song): - """ - ``parent`` - The QWidget-derived parent of the wizard. - - ``song`` - The Song which this SongReviewWidget should represent. - """ - QtGui.QWidget.__init__(self, parent) - self.song = song - self.setupUi() - self.retranslateUi() - QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) - - def setupUi(self): - self.song_vertical_layout = QtGui.QVBoxLayout(self) - self.song_vertical_layout.setObjectName(u'song_vertical_layout') - self.song_group_box = QtGui.QGroupBox(self) - self.song_group_box.setObjectName(u'song_group_box') - self.song_group_box.setMinimumWidth(300) - self.song_group_box.setMaximumWidth(300) - self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) - self.song_group_box_layout.setObjectName(u'song_group_box_layout') - self.song_info_form_layout = QtGui.QFormLayout() - self.song_info_form_layout.setObjectName(u'song_info_form_layout') - #title - self.song_title_label = QtGui.QLabel(self) - self.song_title_label.setObjectName(u'song_title_label') - self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label) - self.song_title_content = QtGui.QLabel(self) - self.song_title_content.setObjectName(u'song_title_content') - self.song_title_content.setText(self.song.title) - self.song_title_content.setWordWrap(True) - self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content) - #alternate title - self.song_alternate_title_label = QtGui.QLabel(self) - self.song_alternate_title_label.setObjectName(u'song_alternate_title_label') - self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label) - self.song_alternate_title_content = QtGui.QLabel(self) - self.song_alternate_title_content.setObjectName(u'song_alternate_title_content') - self.song_alternate_title_content.setText(self.song.alternate_title) - self.song_alternate_title_content.setWordWrap(True) - self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content) - #CCLI number - self.song_ccli_number_label = QtGui.QLabel(self) - self.song_ccli_number_label.setObjectName(u'song_ccli_number_label') - self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label) - self.song_ccli_number_content = QtGui.QLabel(self) - self.song_ccli_number_content.setObjectName(u'song_ccli_number_content') - self.song_ccli_number_content.setText(self.song.ccli_number) - self.song_ccli_number_content.setWordWrap(True) - self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content) - #copyright - self.song_copyright_label = QtGui.QLabel(self) - self.song_copyright_label.setObjectName(u'song_copyright_label') - self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label) - self.song_copyright_content = QtGui.QLabel(self) - self.song_copyright_content.setObjectName(u'song_copyright_content') - self.song_copyright_content.setWordWrap(True) - self.song_copyright_content.setText(self.song.copyright) - self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content) - #comments - self.song_comments_label = QtGui.QLabel(self) - self.song_comments_label.setObjectName(u'song_comments_label') - self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label) - self.song_comments_content = QtGui.QLabel(self) - self.song_comments_content.setObjectName(u'song_comments_content') - self.song_comments_content.setText(self.song.comments) - self.song_comments_content.setWordWrap(True) - self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content) - #authors - self.song_authors_label = QtGui.QLabel(self) - self.song_authors_label.setObjectName(u'song_authors_label') - self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label) - self.song_authors_content = QtGui.QLabel(self) - self.song_authors_content.setObjectName(u'song_authors_content') - self.song_authors_content.setWordWrap(True) - authors_text = u'' - for author in self.song.authors: - authors_text += author.display_name + ', ' - if authors_text: - authors_text = authors_text[:-2] - self.song_authors_content.setText(authors_text) - self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) - #verse order - self.song_verse_order_label = QtGui.QLabel(self) - self.song_verse_order_label.setObjectName(u'song_verse_order_label') - self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label) - self.song_verse_order_content = QtGui.QLabel(self) - self.song_verse_order_content.setObjectName(u'song_verse_order_content') - self.song_verse_order_content.setText(self.song.verse_order) - self.song_verse_order_content.setWordWrap(True) - self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) - #verses - self.song_group_box_layout.addLayout(self.song_info_form_layout) - self.song_info_verse_group_box = QtGui.QGroupBox(self.song_group_box) - self.song_info_verse_group_box.setObjectName(u'song_info_verse_group_box') - self.song_info_verse_group_box_layout = QtGui.QFormLayout(self.song_info_verse_group_box) - song_xml = SongXML() - verses = song_xml.get_verses(self.song.lyrics) - for verse in verses: - verse_marker = verse[0]['type'] + verse[0]['label'] - verse_label = QtGui.QLabel(self.song_info_verse_group_box) - verse_label.setText(verse[1]) - verse_label.setWordWrap(True) - self.song_info_verse_group_box_layout.addRow(verse_marker, verse_label) - self.song_group_box_layout.addWidget(self.song_info_verse_group_box) - self.song_group_box_layout.addStretch() - self.song_vertical_layout.addWidget(self.song_group_box) - self.song_remove_button = QtGui.QPushButton(self) - self.song_remove_button.setObjectName(u'song_remove_button') - self.song_remove_button.setIcon(build_icon(u':/songs/song_delete.png')) - self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - self.song_vertical_layout.addWidget(self.song_remove_button, alignment = QtCore.Qt.AlignHCenter) - - def retranslateUi(self): - self.song_remove_button.setText(u'Remove') - self.song_title_label.setText(u'Title:') - self.song_alternate_title_label.setText(u'Alternate Title:') - self.song_ccli_number_label.setText(u'CCLI Number:') - self.song_verse_order_label.setText(u'Verse Order:') - self.song_copyright_label.setText(u'Copyright:') - self.song_comments_label.setText(u'Comments:') - self.song_authors_label.setText(u'Authors:') - self.song_info_verse_group_box.setTitle(u'Verses') - - def onRemoveButtonClicked(self): - """ - Signal emitted when the "remove" button is clicked. - """ - self.emit(QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self) - diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py new file mode 100644 index 000000000..36b23b8da --- /dev/null +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-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 # +############################################################################### +""" +A widget representing a song in the duplicate song removal wizard review page. +""" +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import build_icon +from openlp.plugins.songs.lib.xml import SongXML + +class SongReviewWidget(QtGui.QWidget): + """ + A widget representing a song on the duplicate song review page. + It displays most of the information a song contains and + provides a "remove" button to remove the song from the database. + The remove logic is not implemented here, but a signal is provided + when the remove button is clicked. + """ + def __init__(self, parent, song): + """ + ``parent`` + The QWidget-derived parent of the wizard. + + ``song`` + The Song which this SongReviewWidget should represent. + """ + QtGui.QWidget.__init__(self, parent) + self.song = song + self.setupUi() + self.retranslateUi() + QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) + + def setupUi(self): + self.song_vertical_layout = QtGui.QVBoxLayout(self) + self.song_vertical_layout.setObjectName(u'song_vertical_layout') + self.song_group_box = QtGui.QGroupBox(self) + self.song_group_box.setObjectName(u'song_group_box') + self.song_group_box.setMinimumWidth(300) + self.song_group_box.setMaximumWidth(300) + self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) + self.song_group_box_layout.setObjectName(u'song_group_box_layout') + self.song_info_form_layout = QtGui.QFormLayout() + self.song_info_form_layout.setObjectName(u'song_info_form_layout') + #title + self.song_title_label = QtGui.QLabel(self) + self.song_title_label.setObjectName(u'song_title_label') + self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label) + self.song_title_content = QtGui.QLabel(self) + self.song_title_content.setObjectName(u'song_title_content') + self.song_title_content.setText(self.song.title) + self.song_title_content.setWordWrap(True) + self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content) + #alternate title + self.song_alternate_title_label = QtGui.QLabel(self) + self.song_alternate_title_label.setObjectName(u'song_alternate_title_label') + self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label) + self.song_alternate_title_content = QtGui.QLabel(self) + self.song_alternate_title_content.setObjectName(u'song_alternate_title_content') + self.song_alternate_title_content.setText(self.song.alternate_title) + self.song_alternate_title_content.setWordWrap(True) + self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content) + #CCLI number + self.song_ccli_number_label = QtGui.QLabel(self) + self.song_ccli_number_label.setObjectName(u'song_ccli_number_label') + self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label) + self.song_ccli_number_content = QtGui.QLabel(self) + self.song_ccli_number_content.setObjectName(u'song_ccli_number_content') + self.song_ccli_number_content.setText(self.song.ccli_number) + self.song_ccli_number_content.setWordWrap(True) + self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content) + #copyright + self.song_copyright_label = QtGui.QLabel(self) + self.song_copyright_label.setObjectName(u'song_copyright_label') + self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label) + self.song_copyright_content = QtGui.QLabel(self) + self.song_copyright_content.setObjectName(u'song_copyright_content') + self.song_copyright_content.setWordWrap(True) + self.song_copyright_content.setText(self.song.copyright) + self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content) + #comments + self.song_comments_label = QtGui.QLabel(self) + self.song_comments_label.setObjectName(u'song_comments_label') + self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label) + self.song_comments_content = QtGui.QLabel(self) + self.song_comments_content.setObjectName(u'song_comments_content') + self.song_comments_content.setText(self.song.comments) + self.song_comments_content.setWordWrap(True) + self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content) + #authors + self.song_authors_label = QtGui.QLabel(self) + self.song_authors_label.setObjectName(u'song_authors_label') + self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label) + self.song_authors_content = QtGui.QLabel(self) + self.song_authors_content.setObjectName(u'song_authors_content') + self.song_authors_content.setWordWrap(True) + authors_text = u'' + for author in self.song.authors: + authors_text += author.display_name + ', ' + if authors_text: + authors_text = authors_text[:-2] + self.song_authors_content.setText(authors_text) + self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) + #verse order + self.song_verse_order_label = QtGui.QLabel(self) + self.song_verse_order_label.setObjectName(u'song_verse_order_label') + self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label) + self.song_verse_order_content = QtGui.QLabel(self) + self.song_verse_order_content.setObjectName(u'song_verse_order_content') + self.song_verse_order_content.setText(self.song.verse_order) + self.song_verse_order_content.setWordWrap(True) + self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) + #verses + self.song_group_box_layout.addLayout(self.song_info_form_layout) + self.song_info_verse_group_box = QtGui.QGroupBox(self.song_group_box) + self.song_info_verse_group_box.setObjectName(u'song_info_verse_group_box') + self.song_info_verse_group_box_layout = QtGui.QFormLayout(self.song_info_verse_group_box) + song_xml = SongXML() + verses = song_xml.get_verses(self.song.lyrics) + for verse in verses: + verse_marker = verse[0]['type'] + verse[0]['label'] + verse_label = QtGui.QLabel(self.song_info_verse_group_box) + verse_label.setText(verse[1]) + verse_label.setWordWrap(True) + self.song_info_verse_group_box_layout.addRow(verse_marker, verse_label) + self.song_group_box_layout.addWidget(self.song_info_verse_group_box) + self.song_group_box_layout.addStretch() + self.song_vertical_layout.addWidget(self.song_group_box) + self.song_remove_button = QtGui.QPushButton(self) + self.song_remove_button.setObjectName(u'song_remove_button') + self.song_remove_button.setIcon(build_icon(u':/songs/song_delete.png')) + self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.song_vertical_layout.addWidget(self.song_remove_button, alignment = QtCore.Qt.AlignHCenter) + + def retranslateUi(self): + self.song_remove_button.setText(u'Remove') + self.song_title_label.setText(u'Title:') + self.song_alternate_title_label.setText(u'Alternate Title:') + self.song_ccli_number_label.setText(u'CCLI Number:') + self.song_verse_order_label.setText(u'Verse Order:') + self.song_copyright_label.setText(u'Copyright:') + self.song_comments_label.setText(u'Comments:') + self.song_authors_label.setText(u'Authors:') + self.song_info_verse_group_box.setTitle(u'Verses') + + def onRemoveButtonClicked(self): + """ + Signal emitted when the "remove" button is clicked. + """ + self.emit(QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self) From 489dc40cba2c20ba7b10704a089a759700691593 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 22:31:43 +0100 Subject: [PATCH 039/235] Make use of the registry for retrieving a widget parent. --- .../songs/forms/duplicatesongremovalform.py | 16 +++++++++++++--- openlp/plugins/songs/songsplugin.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 4e43e94d2..58ce0c96c 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -35,7 +35,7 @@ import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import translate, build_icon +from openlp.core.lib import Registry, translate, build_icon from openlp.core.lib.db import Manager from openlp.core.lib.ui import UiStrings, critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings @@ -53,7 +53,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ log.info(u'DuplicateSongRemovalForm loaded') - def __init__(self, parent, plugin): + def __init__(self, plugin): """ Instantiate the wizard, and run any extra setup we need to. @@ -66,7 +66,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicate_song_list = [] self.review_current_count = 0 self.review_total_count = 0 - OpenLPWizard.__init__(self, parent, plugin, u'duplicateSongRemovalWizard', + OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) def customSignals(self): @@ -346,3 +346,13 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() + + def _get_main_window(self): + """ + Adds the main window to the class dynamically + """ + if not hasattr(self, u'_main_window'): + self._main_window = Registry().get(u'main_window') + return self._main_window + + main_window = property(_get_main_window) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index f17423337..a6dff04c9 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -178,7 +178,7 @@ class SongsPlugin(Plugin): """ Search for duplicates in the song database. """ - DuplicateSongRemovalForm(self.main_window, self).exec_() + DuplicateSongRemovalForm(self).exec_() def onSongImportItemClicked(self): if self.mediaItem: From 8a19e75be3a63a7a25c79fd09ab994ce64562aa9 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 14 Feb 2013 23:17:46 +0100 Subject: [PATCH 040/235] Move remaining methods over to new naming scheme. --- .../songs/forms/duplicatesongremovalform.py | 34 +++++++++---------- .../plugins/songs/forms/songreviewwidget.py | 6 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 58ce0c96c..763e44a65 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -135,11 +135,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): u'explicit approval.')) self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) self.searching_page.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) - self.updateReviewCounterText() + self.update_review_counter_text() self.review_page.setSubTitle(translate(u'Wizard', u'Here you can decide which songs to remove and which ones to keep.')) - def updateReviewCounterText(self): + def update_review_counter_text(self): """ Set the wizard review page header text. """ @@ -161,7 +161,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): if max_songs == 0 or max_songs == 1: self.duplicate_search_progress_bar.setMaximum(1) self.duplicate_search_progress_bar.setValue(1) - self.notifyNoDuplicates() + self.notify_no_duplicates() return # with x songs we have x*(x - 1) / 2 comparisons max_progress_count = max_songs * (max_songs - 1) / 2 @@ -171,7 +171,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): for inner_song_counter in range(outer_song_counter + 1, max_songs): double_finder = DuplicateSongFinder() if double_finder.songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): - duplicate_added = self.addDuplicatesToSongList(songs[outer_song_counter], + duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter], songs[inner_song_counter]) if duplicate_added: self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " + @@ -179,11 +179,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) self.review_total_count = len(self.duplicate_song_list) if self.review_total_count == 0: - self.notifyNoDuplicates() + self.notify_no_duplicates() elif page_id == self.review_page_id: - self.processCurrentDuplicateEntry() + self.process_current_duplicate_entry() - def notifyNoDuplicates(self): + def notify_no_duplicates(self): """ Notifies the user, that there were no duplicates found in the database. """ @@ -195,7 +195,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) - def addDuplicatesToSongList(self, search_song, duplicate_song): + def add_duplicates_to_song_list(self, search_song, duplicate_song): """ Inserts a song duplicate (two similar songs) to the duplicate song list. If one of the two songs is already part of the duplicate song list, @@ -257,11 +257,11 @@ class DuplicateSongRemovalForm(OpenLPWizard): if len(self.duplicate_song_list) == 1: return True else: - self.proceedToNextReview() + self.proceed_to_next_review() return False return OpenLPWizard.validateCurrentPage(self) - def removeButtonClicked(self, song_review_widget): + def remove_button_clicked(self, song_review_widget): """ Removes a song from the database, removes the GUI element representing the song on the review page, and disable the remove button if only one duplicate @@ -299,9 +299,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): if len(self.duplicate_song_list[-1]) == 1: self.songs_horizontal_layout.itemAt(2).widget().song_remove_button.setEnabled(False) - def proceedToNextReview(self): + def proceed_to_next_review(self): """ - Removes the previous review UI elements and calls processCurrentDuplicateEntry. + Removes the previous review UI elements and calls process_current_duplicate_entry. """ #remove last duplicate group self.duplicate_song_list.pop() @@ -317,9 +317,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): else: self.songs_horizontal_layout.removeItem(item) #process next set of duplicates - self.processCurrentDuplicateEntry() + self.process_current_duplicate_entry() - def processCurrentDuplicateEntry(self): + def process_current_duplicate_entry(self): """ Update the review counter in the wizard header, add song widgets for the current duplicate group to review, if it's the last @@ -327,7 +327,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ # update counter self.review_current_count = self.review_total_count - (len(self.duplicate_song_list) - 1) - self.updateReviewCounterText() + self.update_review_counter_text() # add song elements to the UI if len(self.duplicate_song_list) > 0: # a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 @@ -336,8 +336,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): for duplicate in self.duplicate_song_list[-1]: song_review_widget = SongReviewWidget(self.review_page, duplicate) QtCore.QObject.connect(song_review_widget, - QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), - self.removeButtonClicked) + QtCore.SIGNAL(u'song_remove_button_clicked(PyQt_PyObject)'), + self.remove_button_clicked) self.songs_horizontal_layout.addWidget(song_review_widget) self.songs_horizontal_layout.addStretch() self.songs_horizontal_layout.addStretch() diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 36b23b8da..6718013dc 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -54,7 +54,7 @@ class SongReviewWidget(QtGui.QWidget): self.song = song self.setupUi() self.retranslateUi() - QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.onRemoveButtonClicked) + QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.on_remove_button_clicked) def setupUi(self): self.song_vertical_layout = QtGui.QVBoxLayout(self) @@ -168,8 +168,8 @@ class SongReviewWidget(QtGui.QWidget): self.song_authors_label.setText(u'Authors:') self.song_info_verse_group_box.setTitle(u'Verses') - def onRemoveButtonClicked(self): + def on_remove_button_clicked(self): """ Signal emitted when the "remove" button is clicked. """ - self.emit(QtCore.SIGNAL(u'songRemoveButtonClicked(PyQt_PyObject)'), self) + self.emit(QtCore.SIGNAL(u'song_remove_button_clicked(PyQt_PyObject)'), self) From 1b6a54c55d64ee1f47d3276c15450cbba3ff838d Mon Sep 17 00:00:00 2001 From: phill-ridout Date: Fri, 15 Feb 2013 19:57:05 +0000 Subject: [PATCH 041/235] 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 042/235] 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 043/235] 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 10d9b506a6fdb0dacc3c35d488af273eed3957fb Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 18 Feb 2013 19:46:50 +0100 Subject: [PATCH 044/235] Even more variables adapted to PEP8. Sentencify lots of comments. --- openlp/core/ui/wizard.py | 14 +++---- .../songs/forms/duplicatesongremovalform.py | 42 +++++++++---------- .../plugins/songs/forms/songreviewwidget.py | 16 +++---- .../plugins/songs/lib/duplicatesongfinder.py | 9 ++-- openlp/plugins/songs/songsplugin.py | 16 +++---- .../openlp_plugins/songs/test_lib.py | 34 +++++++-------- 6 files changed, 65 insertions(+), 66 deletions(-) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index d269ce9fd..3a2596a77 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -93,16 +93,16 @@ class OpenLPWizard(QtGui.QWizard): ``image`` The image to display on the "welcome" page of the wizard. Should be 163x350. - ``addProgressPage`` + ``add_progress_page`` Whether to add a progress page with a progressbar at the end of the wizard. """ - def __init__(self, parent, plugin, name, image, addProgressPage=True): + def __init__(self, parent, plugin, name, image, add_progress_page=True): """ Constructor """ QtGui.QWizard.__init__(self, parent) self.plugin = plugin - self.withProgressPage = addProgressPage + self.with_progress_page = add_progress_page self.setObjectName(name) self.openIcon = build_icon(u':/general/general_open.png') self.deleteIcon = build_icon(u':/general/general_delete.png') @@ -113,7 +113,7 @@ class OpenLPWizard(QtGui.QWizard): self.customInit() self.customSignals() QtCore.QObject.connect(self, QtCore.SIGNAL(u'currentIdChanged(int)'), self.onCurrentIdChanged) - if self.withProgressPage: + if self.with_progress_page: QtCore.QObject.connect(self.errorCopyToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorCopyToButtonClicked) QtCore.QObject.connect(self.errorSaveToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorSaveToButtonClicked) @@ -128,7 +128,7 @@ class OpenLPWizard(QtGui.QWizard): QtGui.QWizard.NoBackButtonOnLastPage) add_welcome_page(self, image) self.addCustomPages() - if self.withProgressPage: + if self.with_progress_page: self.addProgressPage() self.retranslateUi() @@ -191,7 +191,7 @@ class OpenLPWizard(QtGui.QWizard): Stop the wizard on cancel button, close button or ESC key. """ log.debug(u'Wizard cancelled by user.') - if self.withProgressPage and self.currentPage() == self.progressPage: + if self.with_progress_page and self.currentPage() == self.progressPage: Registry().execute(u'openlp_stop_wizard') self.done(QtGui.QDialog.Rejected) @@ -199,7 +199,7 @@ class OpenLPWizard(QtGui.QWizard): """ Perform necessary functions depending on which wizard page is active. """ - if self.withProgressPage and self.page(pageId) == self.progressPage: + if self.with_progress_page and self.page(pageId) == self.progressPage: self.preWizard() self.performWizard() self.postWizard() diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 763e44a65..a0406160c 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -80,7 +80,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Add song wizard specific pages. """ - #add custom pages + # Add custom pages. self.searching_page = QtGui.QWizardPage() self.searching_page.setObjectName(u'searching_page') self.searching_vertical_layout = QtGui.QVBoxLayout(self.searching_page) @@ -117,8 +117,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songs_horizontal_scroll_area.setWidget(self.songs_horizontal_songs_widget) self.review_layout.addWidget(self.songs_horizontal_scroll_area) self.review_page_id = self.addPage(self.review_page) - #add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the - #review page + # Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the + #review page. self.dummy_page = QtGui.QWizardPage() self.dummy_page_id = self.addPage(self.dummy_page) @@ -153,17 +153,17 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``page_id`` ID of the page the wizard changed to. """ - #hide back button + # Hide back button. self.button(QtGui.QWizard.BackButton).hide() if page_id == self.searching_page_id: - #search duplicate songs + # Search duplicate songs. max_songs = self.plugin.manager.get_object_count(Song) if max_songs == 0 or max_songs == 1: self.duplicate_search_progress_bar.setMaximum(1) self.duplicate_search_progress_bar.setValue(1) self.notify_no_duplicates() return - # with x songs we have x*(x - 1) / 2 comparisons + # With x songs we have x*(x - 1) / 2 comparisons. max_progress_count = max_songs * (max_songs - 1) / 2 self.duplicate_search_progress_bar.setMaximum(max_progress_count) songs = self.plugin.manager.get_all_objects(Song) @@ -212,7 +212,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): duplicate_group_found = False duplicate_added = False for duplicate_group in self.duplicate_song_list: - #skip the first song in the duplicate lists, since the first one has to be an earlier song + # Skip the first song in the duplicate lists, since the first one has to be an earlier song. if search_song in duplicate_group and not duplicate_song in duplicate_group: duplicate_group.append(duplicate_song) duplicate_group_found = True @@ -253,7 +253,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): on the review page as long as there are more song duplicates to review. """ if self.currentId() == self.review_page_id: - #as long as it's not the last duplicate list entry we revisit the review page + # As long as it's not the last duplicate list entry we revisit the review page. if len(self.duplicate_song_list) == 1: return True else: @@ -270,9 +270,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): ``song_review_widget`` The SongReviewWidget whose song we should delete. """ - #remove song from duplicate song list + # Remove song from duplicate song list. self.duplicate_song_list[-1].remove(song_review_widget.song) - #remove song + # Remove song from the database. item_id = song_review_widget.song.id media_files = self.plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == item_id) @@ -290,12 +290,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): except OSError: log.exception(u'Could not remove directory: %s', save_path) self.plugin.manager.delete_object(Song, item_id) - # remove GUI elements + # Remove GUI elements for the song. self.songs_horizontal_layout.removeWidget(song_review_widget) song_review_widget.setParent(None) - # check if we only have one duplicate left + # Check if we only have one duplicate left: # 4 stretches + 1 SongReviewWidget = 5 - # the SongReviewWidget is then at position 2 + # The SongReviewWidget is then at position 2. if len(self.duplicate_song_list[-1]) == 1: self.songs_horizontal_layout.itemAt(2).widget().song_remove_button.setEnabled(False) @@ -303,9 +303,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Removes the previous review UI elements and calls process_current_duplicate_entry. """ - #remove last duplicate group + # Remove last duplicate group. self.duplicate_song_list.pop() - # remove all previous elements + # Remove all previous elements. for i in reversed(range(self.songs_horizontal_layout.count())): item = self.songs_horizontal_layout.itemAt(i) if isinstance(item, QtGui.QWidgetItem): @@ -316,7 +316,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): widget.setParent(None) else: self.songs_horizontal_layout.removeItem(item) - #process next set of duplicates + # Process next set of duplicates. self.process_current_duplicate_entry() def process_current_duplicate_entry(self): @@ -325,12 +325,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): the current duplicate group to review, if it's the last duplicate song group, hide the "next" button and show the "finish" button. """ - # update counter + # Update the counter. self.review_current_count = self.review_total_count - (len(self.duplicate_song_list) - 1) self.update_review_counter_text() - # add song elements to the UI + # Add song elements to the UI. if len(self.duplicate_song_list) > 0: - # a stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050 + # A stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050. self.songs_horizontal_layout.addStretch() self.songs_horizontal_layout.addStretch() for duplicate in self.duplicate_song_list[-1]: @@ -341,7 +341,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songs_horizontal_layout.addWidget(song_review_widget) self.songs_horizontal_layout.addStretch() self.songs_horizontal_layout.addStretch() - #change next button to finish button on last review + # Change next button to finish button on last review. if len(self.duplicate_song_list) == 1: self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) @@ -349,7 +349,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): def _get_main_window(self): """ - Adds the main window to the class dynamically + Adds the main window to the class dynamically. """ if not hasattr(self, u'_main_window'): self._main_window = Registry().get(u'main_window') diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 6718013dc..81154750a 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -67,7 +67,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_group_box_layout.setObjectName(u'song_group_box_layout') self.song_info_form_layout = QtGui.QFormLayout() self.song_info_form_layout.setObjectName(u'song_info_form_layout') - #title + # Add title widget. self.song_title_label = QtGui.QLabel(self) self.song_title_label.setObjectName(u'song_title_label') self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label) @@ -76,7 +76,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_title_content.setText(self.song.title) self.song_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content) - #alternate title + # Add alternate title widget. self.song_alternate_title_label = QtGui.QLabel(self) self.song_alternate_title_label.setObjectName(u'song_alternate_title_label') self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label) @@ -85,7 +85,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_alternate_title_content.setText(self.song.alternate_title) self.song_alternate_title_content.setWordWrap(True) self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content) - #CCLI number + # Add CCLI number widget. self.song_ccli_number_label = QtGui.QLabel(self) self.song_ccli_number_label.setObjectName(u'song_ccli_number_label') self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label) @@ -94,7 +94,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_ccli_number_content.setText(self.song.ccli_number) self.song_ccli_number_content.setWordWrap(True) self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content) - #copyright + # Add copyright widget. self.song_copyright_label = QtGui.QLabel(self) self.song_copyright_label.setObjectName(u'song_copyright_label') self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label) @@ -103,7 +103,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_copyright_content.setWordWrap(True) self.song_copyright_content.setText(self.song.copyright) self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content) - #comments + # Add comments widget. self.song_comments_label = QtGui.QLabel(self) self.song_comments_label.setObjectName(u'song_comments_label') self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label) @@ -112,7 +112,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_comments_content.setText(self.song.comments) self.song_comments_content.setWordWrap(True) self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content) - #authors + # Add authors widget. self.song_authors_label = QtGui.QLabel(self) self.song_authors_label.setObjectName(u'song_authors_label') self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label) @@ -126,7 +126,7 @@ class SongReviewWidget(QtGui.QWidget): authors_text = authors_text[:-2] self.song_authors_content.setText(authors_text) self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) - #verse order + # Add verse order widget. self.song_verse_order_label = QtGui.QLabel(self) self.song_verse_order_label.setObjectName(u'song_verse_order_label') self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label) @@ -135,7 +135,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_verse_order_content.setText(self.song.verse_order) self.song_verse_order_content.setWordWrap(True) self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) - #verses + # Add verses widget. self.song_group_box_layout.addLayout(self.song_info_form_layout) self.song_info_verse_group_box = QtGui.QGroupBox(self.song_group_box) self.song_info_verse_group_box.setObjectName(u'song_info_verse_group_box') diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py index 38062632d..225789109 100644 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ b/openlp/plugins/songs/lib/duplicatesongfinder.py @@ -73,7 +73,6 @@ class DuplicateSongFinder(object): differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() diff_no_typos = self.__remove_typos(diff_tuples) - #print(diff_no_typos) if self.__length_of_equal_blocks(diff_no_typos) >= self.min_block_size or \ self.__length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: return True @@ -97,25 +96,25 @@ class DuplicateSongFinder(object): ``diff`` The diff set to remove the typos from. """ - #remove typo at beginning of string + # Remove typo at beginning of the string. if len(diff) >= 2: if diff[0][0] != "equal" and self.__op_length(diff[0]) <= self.max_typo_size and \ self.__op_length(diff[1]) >= self.min_fragment_size: del diff[0] - #remove typos in the middle of string + # Remove typos in the middle of the string. if len(diff) >= 3: for index in range(len(diff) - 3, -1, -1): if self.__op_length(diff[index]) >= self.min_fragment_size and \ diff[index + 1][0] != "equal" and self.__op_length(diff[index + 1]) <= self.max_typo_size and \ self.__op_length(diff[index + 2]) >= self.min_fragment_size: del diff[index + 1] - #remove typo at the end of string + # Remove typo at the end of the string. if len(diff) >= 2: if self.__op_length(diff[-2]) >= self.min_fragment_size and \ diff[-1][0] != "equal" and self.__op_length(diff[-1]) <= self.max_typo_size: del diff[-1] - #merge fragments + # Merge the bordering equal passages that occured by removing differences. for index in range(len(diff) - 2, -1, -1): if diff[index][0] == "equal" and self.__op_length(diff[index]) >= self.min_fragment_size and \ diff[index + 1][0] == "equal" and self.__op_length(diff[index + 1]) >= self.min_fragment_size: diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index a6dff04c9..237b898ba 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -94,12 +94,12 @@ class SongsPlugin(Plugin): self.songImportItem.setVisible(True) self.songExportItem.setVisible(True) self.toolsReindexItem.setVisible(True) - self.toolsFindDuplicates.setVisible(True) + self.tools_find_duplicates.setVisible(True) action_list = ActionList.get_instance() action_list.add_action(self.songImportItem, UiStrings().Import) action_list.add_action(self.songExportItem, UiStrings().Export) action_list.add_action(self.toolsReindexItem, UiStrings().Tools) - action_list.add_action(self.toolsFindDuplicates, UiStrings().Tools) + action_list.add_action(self.tools_find_duplicates, UiStrings().Tools) def addImportMenuItem(self, import_menu): """ @@ -149,12 +149,12 @@ class SongsPlugin(Plugin): statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'), visible=False, triggers=self.onToolsReindexItemTriggered) tools_menu.addAction(self.toolsReindexItem) - self.toolsFindDuplicates = create_action(tools_menu, u'toolsFindDuplicates', + self.tools_find_duplicates = create_action(tools_menu, u'toolsFindDuplicates', text=translate('SongsPlugin', 'Find &Duplicate Songs'), statustip=translate('SongsPlugin', 'Find and remove duplicate songs in the song database.'), - visible=False, triggers=self.onToolsFindDuplicatesTriggered) - tools_menu.addAction(self.toolsFindDuplicates) + visible=False, triggers=self.on_tools_find_duplicates_triggered) + tools_menu.addAction(self.tools_find_duplicates) def onToolsReindexItemTriggered(self): """ @@ -174,7 +174,7 @@ class SongsPlugin(Plugin): self.manager.save_objects(songs) self.mediaItem.onSearchTextButtonClicked() - def onToolsFindDuplicatesTriggered(self): + def on_tools_find_duplicates_triggered(self): """ Search for duplicates in the song database. """ @@ -300,12 +300,12 @@ class SongsPlugin(Plugin): self.songImportItem.setVisible(False) self.songExportItem.setVisible(False) self.toolsReindexItem.setVisible(False) - self.toolsFindDuplicates.setVisible(False) + self.tools_find_duplicates.setVisible(False) action_list = ActionList.get_instance() action_list.remove_action(self.songImportItem, UiStrings().Import) action_list.remove_action(self.songExportItem, UiStrings().Export) action_list.remove_action(self.toolsReindexItem, UiStrings().Tools) - action_list.remove_action(self.toolsFindDuplicates, UiStrings().Tools) + action_list.remove_action(self.tools_find_duplicates, UiStrings().Tools) Plugin.finalise(self) def new_service_created(self): diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 6de41da37..284a14859 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -53,46 +53,46 @@ class TestLib(TestCase): that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a crown''' - dsf = DuplicateSongFinder() + duplicate_song_finder = DuplicateSongFinder() song1 = MagicMock() song2 = MagicMock() - #GIVEN: Two equal songs + #GIVEN: Two equal songs. song1.search_lyrics = full_lyrics song2.search_lyrics = full_lyrics - #WHEN: We compare those songs for equality - result = dsf.songs_probably_equal(song1, song2) + #WHEN: We compare those songs for equality. + result = duplicate_song_finder.songs_probably_equal(song1, song2) - #THEN: The result should be True + #THEN: The result should be True. assert result is True, u'The result should be True' - #GIVEN: A song and a short version of the same song + #GIVEN: A song and a short version of the same song. song1.search_lyrics = full_lyrics song2.search_lyrics = short_lyrics - #WHEN: We compare those songs for equality - result = dsf.songs_probably_equal(song1, song2) + #WHEN: We compare those songs for equality. + result = duplicate_song_finder.songs_probably_equal(song1, song2) - #THEN: The result should be True + #THEN: The result should be True. assert result is True, u'The result should be True' - #GIVEN: A song and the same song with lots of errors + #GIVEN: A song and the same song with lots of errors. song1.search_lyrics = full_lyrics song2.search_lyrics = error_lyrics - #WHEN: We compare those songs for equality - result = dsf.songs_probably_equal(song1, song2) + #WHEN: We compare those songs for equality. + result = duplicate_song_finder.songs_probably_equal(song1, song2) - #THEN: The result should be True + #THEN: The result should be True. assert result is True, u'The result should be True' - #GIVEN: Two different songs + #GIVEN: Two different songs. song1.search_lyrics = full_lyrics song2.search_lyrics = different_lyrics - #WHEN: We compare those songs for equality - result = dsf.songs_probably_equal(song1, song2) + #WHEN: We compare those songs for equality. + result = duplicate_song_finder.songs_probably_equal(song1, song2) - #THEN: The result should be False + #THEN: The result should be False. assert result is False, u'The result should be False' From 0c78afc8d6624dbab903859a2a662d92897b18a4 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 18 Feb 2013 19:52:56 +0100 Subject: [PATCH 045/235] Simplify a junk of code with a very elegant join snippet by googol++. --- openlp/plugins/songs/forms/songreviewwidget.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 81154750a..9799a9ee7 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -119,11 +119,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_authors_content = QtGui.QLabel(self) self.song_authors_content.setObjectName(u'song_authors_content') self.song_authors_content.setWordWrap(True) - authors_text = u'' - for author in self.song.authors: - authors_text += author.display_name + ', ' - if authors_text: - authors_text = authors_text[:-2] + authors_text = u', '.join([author.display_name for author in self.song.authors]) self.song_authors_content.setText(authors_text) self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content) # Add verse order widget. From 8c8cd3b8674e313d1853441a5dce9ee6b85ffe78 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 18 Feb 2013 21:27:11 +0100 Subject: [PATCH 046/235] Replace DuplicateSongFinder class with a set of functions. --- .../songs/forms/duplicatesongremovalform.py | 10 +- .../plugins/songs/lib/duplicatesongfinder.py | 153 ------------------ .../openlp_plugins/songs/test_lib.py | 15 +- 3 files changed, 10 insertions(+), 168 deletions(-) delete mode 100644 openlp/plugins/songs/lib/duplicatesongfinder.py diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index a0406160c..3ec93f25f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -29,20 +29,17 @@ """ The duplicate song removal logic for OpenLP. """ -import codecs import logging import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import Registry, translate, build_icon -from openlp.core.lib.db import Manager -from openlp.core.lib.ui import UiStrings, critical_error_message_box +from openlp.core.lib import Registry, translate from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.core.utils import AppLocation from openlp.plugins.songs.lib.db import Song, MediaFile -from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget +from openlp.plugins.songs.lib.songcompare import songs_probably_equal log = logging.getLogger(__name__) @@ -169,8 +166,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): songs = self.plugin.manager.get_all_objects(Song) for outer_song_counter in range(max_songs - 1): for inner_song_counter in range(outer_song_counter + 1, max_songs): - double_finder = DuplicateSongFinder() - if double_finder.songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): + if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter], songs[inner_song_counter]) if duplicate_added: diff --git a/openlp/plugins/songs/lib/duplicatesongfinder.py b/openlp/plugins/songs/lib/duplicatesongfinder.py deleted file mode 100644 index 225789109..000000000 --- a/openlp/plugins/songs/lib/duplicatesongfinder.py +++ /dev/null @@ -1,153 +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 # -############################################################################### -import difflib - -from openlp.plugins.songs.lib.db import Song - -class DuplicateSongFinder(object): - """ - The :class:`DuplicateSongFinder` class provides functionality to search for - duplicate songs. - - The algorithm is based on the diff algorithm. - First a diffset is calculated for two songs. - To compensate for typos all differences that are smaller than a - limit (min_fragment_size) are removed and the surrounding equal parts are merged. - Finally two conditions can qualify a song tuple to be a duplicate: - 1. There is a block of equal content that is at least min_block_size large. - This condition should hit for all larger songs that have a long enough - equal part. Even if only one verse is equal this condition should still hit. - 2. Two thirds of the smaller song is contained in the larger song. - This condition should hit if one of the two songs (or both) is small (smaller - than the min_block_size), but most of the song is contained in the other song. - """ - - def __init__(self): - self.min_fragment_size = 5 - self.min_block_size = 70 - self.max_typo_size = 3 - - def songs_probably_equal(self, song1, song2): - """ - Calculate and return whether two songs are probably equal. - - ``song1`` - The first song to compare. - - ``song2`` - The second song to compare. - """ - if len(song1.search_lyrics) < len(song2.search_lyrics): - small = song1.search_lyrics - large = song2.search_lyrics - else: - small = song2.search_lyrics - large = song1.search_lyrics - differ = difflib.SequenceMatcher(a=large, b=small) - diff_tuples = differ.get_opcodes() - diff_no_typos = self.__remove_typos(diff_tuples) - if self.__length_of_equal_blocks(diff_no_typos) >= self.min_block_size or \ - self.__length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: - return True - else: - return False - - def __op_length(self, opcode): - """ - Return the length of a given difference. - - ``opcode`` - The difference. - """ - return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) - - def __remove_typos(self, diff): - """ - Remove typos from a diff set. A typo is a small difference (min_fragment_size). - - ``diff`` - The diff set to remove the typos from. - """ - # Remove typo at beginning of the string. - if len(diff) >= 2: - if diff[0][0] != "equal" and self.__op_length(diff[0]) <= self.max_typo_size and \ - self.__op_length(diff[1]) >= self.min_fragment_size: - del diff[0] - # Remove typos in the middle of the string. - if len(diff) >= 3: - for index in range(len(diff) - 3, -1, -1): - if self.__op_length(diff[index]) >= self.min_fragment_size and \ - diff[index + 1][0] != "equal" and self.__op_length(diff[index + 1]) <= self.max_typo_size and \ - self.__op_length(diff[index + 2]) >= self.min_fragment_size: - del diff[index + 1] - # Remove typo at the end of the string. - if len(diff) >= 2: - if self.__op_length(diff[-2]) >= self.min_fragment_size and \ - diff[-1][0] != "equal" and self.__op_length(diff[-1]) <= self.max_typo_size: - del diff[-1] - - # Merge the bordering equal passages that occured by removing differences. - for index in range(len(diff) - 2, -1, -1): - if diff[index][0] == "equal" and self.__op_length(diff[index]) >= self.min_fragment_size and \ - diff[index + 1][0] == "equal" and self.__op_length(diff[index + 1]) >= self.min_fragment_size: - diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], - diff[index + 1][4]) - del diff[index + 1] - - return diff - - def __length_of_equal_blocks(self, diff): - """ - Return the total length of all equal blocks in a diff set. - Blocks smaller than min_block_size are not counted. - - ``diff`` - The diff set to return the length for. - """ - length = 0 - for element in diff: - if element[0] == "equal" and self.__op_length(element) >= self.min_block_size: - length += self.__op_length(element) - return length - - def __length_of_longest_equal_block(self, diff): - """ - Return the length of the largest equal block in a diff set. - - ``diff`` - The diff set to return the length for. - """ - length = 0 - for element in diff: - if element[0] == "equal" and self.__op_length(element) > length: - length = self.__op_length(element) - return length - diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 284a14859..9f04e7350 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -31,13 +31,13 @@ from unittest import TestCase from mock import MagicMock -from openlp.plugins.songs.lib.duplicatesongfinder import DuplicateSongFinder +from openlp.plugins.songs.lib.songcompare import songs_probably_equal class TestLib(TestCase): def songs_probably_equal_test(self): """ - Test the DuplicateSongFinder.songs_probably_equal function. + Test the songs_probably_equal function. """ full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how @@ -53,7 +53,6 @@ class TestLib(TestCase): that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a crown''' - duplicate_song_finder = DuplicateSongFinder() song1 = MagicMock() song2 = MagicMock() @@ -62,7 +61,7 @@ class TestLib(TestCase): song2.search_lyrics = full_lyrics #WHEN: We compare those songs for equality. - result = duplicate_song_finder.songs_probably_equal(song1, song2) + result = songs_probably_equal(song1, song2) #THEN: The result should be True. assert result is True, u'The result should be True' @@ -72,17 +71,17 @@ class TestLib(TestCase): song2.search_lyrics = short_lyrics #WHEN: We compare those songs for equality. - result = duplicate_song_finder.songs_probably_equal(song1, song2) + result = songs_probably_equal(song1, song2) #THEN: The result should be True. - assert result is True, u'The result should be True' + assert result is True, u'The result should be True' #GIVEN: A song and the same song with lots of errors. song1.search_lyrics = full_lyrics song2.search_lyrics = error_lyrics #WHEN: We compare those songs for equality. - result = duplicate_song_finder.songs_probably_equal(song1, song2) + result = songs_probably_equal(song1, song2) #THEN: The result should be True. assert result is True, u'The result should be True' @@ -92,7 +91,7 @@ class TestLib(TestCase): song2.search_lyrics = different_lyrics #WHEN: We compare those songs for equality. - result = duplicate_song_finder.songs_probably_equal(song1, song2) + result = songs_probably_equal(song1, song2) #THEN: The result should be False. assert result is False, u'The result should be False' From 904620998f3d286353ebbf068a7c6b1fc02d956f Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 18 Feb 2013 22:42:04 +0100 Subject: [PATCH 047/235] Remove non-needed test __init__ file. Split up testfunctions. --- openlp/plugins/songs/lib/songcompare.py | 153 ++++++++++++++++++ .../openlp_plugins/songs/__init__.py | 8 - .../openlp_plugins/songs/test_lib.py | 86 ++++++---- 3 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 openlp/plugins/songs/lib/songcompare.py delete mode 100644 tests/functional/openlp_plugins/songs/__init__.py diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py new file mode 100644 index 000000000..e230d9f53 --- /dev/null +++ b/openlp/plugins/songs/lib/songcompare.py @@ -0,0 +1,153 @@ +# -*- 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:`songcompare` module provides functionality to search for +duplicate songs. It has one single :function:`songs_probably_equal`. + +The algorithm is based on the diff algorithm. +First a diffset is calculated for two songs. +To compensate for typos all differences that are smaller than a +limit (min_fragment_size) are removed and the surrounding equal parts are merged. +Finally two conditions can qualify a song tuple to be a duplicate: +1. There is a block of equal content that is at least min_block_size large. + This condition should hit for all larger songs that have a long enough + equal part. Even if only one verse is equal this condition should still hit. +2. Two thirds of the smaller song is contained in the larger song. + This condition should hit if one of the two songs (or both) is small (smaller + than the min_block_size), but most of the song is contained in the other song. +""" +import difflib + + +min_fragment_size = 5 +min_block_size = 70 +max_typo_size = 3 + + +def songs_probably_equal(song1, song2): + """ + Calculate and return whether two songs are probably equal. + + ``song1`` + The first song to compare. + + ``song2`` + The second song to compare. + """ + if len(song1.search_lyrics) < len(song2.search_lyrics): + small = song1.search_lyrics + large = song2.search_lyrics + else: + small = song2.search_lyrics + large = song1.search_lyrics + differ = difflib.SequenceMatcher(a=large, b=small) + diff_tuples = differ.get_opcodes() + diff_no_typos = __remove_typos(diff_tuples) + if __length_of_equal_blocks(diff_no_typos) >= min_block_size or \ + __length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: + return True + else: + return False + + +def __op_length(opcode): + """ + Return the length of a given difference. + + ``opcode`` + The difference. + """ + return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) + + +def __remove_typos(diff): + """ + Remove typos from a diff set. A typo is a small difference (min_fragment_size). + + ``diff`` + The diff set to remove the typos from. + """ + # Remove typo at beginning of the string. + if len(diff) >= 2: + if diff[0][0] != "equal" and __op_length(diff[0]) <= max_typo_size and \ + __op_length(diff[1]) >= min_fragment_size: + del diff[0] + # Remove typos in the middle of the string. + if len(diff) >= 3: + for index in range(len(diff) - 3, -1, -1): + if __op_length(diff[index]) >= min_fragment_size and \ + diff[index + 1][0] != "equal" and __op_length(diff[index + 1]) <= max_typo_size and \ + __op_length(diff[index + 2]) >= min_fragment_size: + del diff[index + 1] + # Remove typo at the end of the string. + if len(diff) >= 2: + if __op_length(diff[-2]) >= min_fragment_size and \ + diff[-1][0] != "equal" and __op_length(diff[-1]) <= max_typo_size: + del diff[-1] + + # Merge the bordering equal passages that occured by removing differences. + for index in range(len(diff) - 2, -1, -1): + if diff[index][0] == "equal" and __op_length(diff[index]) >= min_fragment_size and \ + diff[index + 1][0] == "equal" and __op_length(diff[index + 1]) >= min_fragment_size: + diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], + diff[index + 1][4]) + del diff[index + 1] + + return diff + + +def __length_of_equal_blocks(diff): + """ + Return the total length of all equal blocks in a diff set. + Blocks smaller than min_block_size are not counted. + + ``diff`` + The diff set to return the length for. + """ + length = 0 + for element in diff: + if element[0] == "equal" and __op_length(element) >= min_block_size: + length += __op_length(element) + return length + + +def __length_of_longest_equal_block(diff): + """ + Return the length of the largest equal block in a diff set. + + ``diff`` + The diff set to return the length for. + """ + length = 0 + for element in diff: + if element[0] == "equal" and __op_length(element) > length: + length = __op_length(element) + return length diff --git a/tests/functional/openlp_plugins/songs/__init__.py b/tests/functional/openlp_plugins/songs/__init__.py deleted file mode 100644 index 0157fb2f0..000000000 --- a/tests/functional/openlp_plugins/songs/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -import sip -sip.setapi(u'QDate', 2) -sip.setapi(u'QDateTime', 2) -sip.setapi(u'QString', 2) -sip.setapi(u'QTextStream', 2) -sip.setapi(u'QTime', 2) -sip.setapi(u'QUrl', 2) -sip.setapi(u'QVariant', 2) diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index 9f04e7350..a02563b22 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -34,64 +34,82 @@ from mock import MagicMock from openlp.plugins.songs.lib.songcompare import songs_probably_equal class TestLib(TestCase): + def setUp(self): + """ + Mock up two songs and provide a set of lyrics for the songs_probably_equal tests. + """ + self.full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am + found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how + precious did that grace appear the hour i first believed through many dangers toils and snares i have already + come tis grace that brought me safe thus far and grace will lead me home''' + self.short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that + grace appear the hour i first believed''' + self.error_lyrics =u'''amazing how sweet the trumpet that saved a wrench like me i once was losst but now am + found waf blind but now i see it was grace that taught my heart to fear and grace my fears relieved how + precious did that grace appppppppear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have + already come to this grace that brought me safe so far and grace will lead me home''' + self.different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love + that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged + cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a + crown''' + self.song1 = MagicMock() + self.song2 = MagicMock() - def songs_probably_equal_test(self): + def songs_probably_equal_same_song_test(self): """ - Test the songs_probably_equal function. + Test the songs_probably_equal function with twice the same song. """ - full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am - found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how - precious did that grace appear the hour i first believed through many dangers toils and snares i have already - come tis grace that brought me safe thus far and grace will lead me home''' - short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that - grace appear the hour i first believed''' - error_lyrics =u'''amazing how sweet the trumpet that saved a wrench like me i once was losst but now am - found waf blind but now i see it was grace that taught my heart to fear and grace my fears relieved how - precious did that grace appppppppear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have - already come to this grace that brought me safe so far and grace will lead me home''' - different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love - that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged - cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a - crown''' - song1 = MagicMock() - song2 = MagicMock() - #GIVEN: Two equal songs. - song1.search_lyrics = full_lyrics - song2.search_lyrics = full_lyrics + self.song1.search_lyrics = self.full_lyrics + self.song2.search_lyrics = self.full_lyrics #WHEN: We compare those songs for equality. - result = songs_probably_equal(song1, song2) + result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. assert result is True, u'The result should be True' - + + + def songs_probably_equal_short_song_test(self): + """ + Test the songs_probably_equal function with a song and a shorter version of the same song. + """ #GIVEN: A song and a short version of the same song. - song1.search_lyrics = full_lyrics - song2.search_lyrics = short_lyrics + self.song1.search_lyrics = self.full_lyrics + self.song2.search_lyrics = self.short_lyrics #WHEN: We compare those songs for equality. - result = songs_probably_equal(song1, song2) + result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. assert result is True, u'The result should be True' - + + + def songs_probably_equal_error_song_test(self): + """ + Test the songs_probably_equal function with a song and a very erroneous version of the same song. + """ #GIVEN: A song and the same song with lots of errors. - song1.search_lyrics = full_lyrics - song2.search_lyrics = error_lyrics + self.song1.search_lyrics = self.full_lyrics + self.song2.search_lyrics = self.error_lyrics #WHEN: We compare those songs for equality. - result = songs_probably_equal(song1, song2) + result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. assert result is True, u'The result should be True' - + + + def songs_probably_equal_different_song_test(self): + """ + Test the songs_probably_equal function with two different songs. + """ #GIVEN: Two different songs. - song1.search_lyrics = full_lyrics - song2.search_lyrics = different_lyrics + self.song1.search_lyrics = self.full_lyrics + self.song2.search_lyrics = self.different_lyrics #WHEN: We compare those songs for equality. - result = songs_probably_equal(song1, song2) + result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be False. assert result is False, u'The result should be False' From 0df1f3932af2d710c61495849da47e514ffbce57 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 18 Feb 2013 23:26:27 +0100 Subject: [PATCH 048/235] Replace QObject.connect calls with a smarter more pythonic syntax. Courtesy of TRB143++. --- openlp/core/ui/wizard.py | 6 +++--- openlp/plugins/songs/forms/duplicatesongremovalform.py | 4 ++-- openlp/plugins/songs/forms/songreviewwidget.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 3a2596a77..de73ec005 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -112,10 +112,10 @@ class OpenLPWizard(QtGui.QWizard): self.registerFields() self.customInit() self.customSignals() - QtCore.QObject.connect(self, QtCore.SIGNAL(u'currentIdChanged(int)'), self.onCurrentIdChanged) + self.currentIdChanged.connect(self.onCurrentIdChanged) if self.with_progress_page: - QtCore.QObject.connect(self.errorCopyToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorCopyToButtonClicked) - QtCore.QObject.connect(self.errorSaveToButton, QtCore.SIGNAL(u'clicked()'), self.onErrorSaveToButtonClicked) + self.errorCopyToButton.clicked.connect(self.onErrorCopyToButtonClicked) + self.errorSaveToButton.clicked.connect(self.onErrorSaveToButtonClicked) def setupUi(self, image): """ diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 3ec93f25f..29eb1b312 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -70,8 +70,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): """ Song wizard specific signals. """ - QtCore.QObject.connect(self.finishButton, QtCore.SIGNAL(u'clicked()'), self.onWizardExit) - QtCore.QObject.connect(self.cancelButton, QtCore.SIGNAL(u'clicked()'), self.onWizardExit) + self.finishButton.clicked.connect(self.onWizardExit) + self.cancelButton.clicked.connect(self.onWizardExit) def addCustomPages(self): """ diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 9799a9ee7..af861205e 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -54,7 +54,7 @@ class SongReviewWidget(QtGui.QWidget): self.song = song self.setupUi() self.retranslateUi() - QtCore.QObject.connect(self.song_remove_button, QtCore.SIGNAL(u'clicked()'), self.on_remove_button_clicked) + self.song_remove_button.clicked.connect(self.on_remove_button_clicked) def setupUi(self): self.song_vertical_layout = QtGui.QVBoxLayout(self) From 3c8c136d3d1fbaa2f076c77bb4ae4f6f72909743 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 19 Feb 2013 23:58:29 +0100 Subject: [PATCH 049/235] Change remove button signal emission to new style signals. --- .../songs/forms/duplicatesongremovalform.py | 4 +--- openlp/plugins/songs/forms/songreviewwidget.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 29eb1b312..4642d6fdb 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -331,9 +331,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songs_horizontal_layout.addStretch() for duplicate in self.duplicate_song_list[-1]: song_review_widget = SongReviewWidget(self.review_page, duplicate) - QtCore.QObject.connect(song_review_widget, - QtCore.SIGNAL(u'song_remove_button_clicked(PyQt_PyObject)'), - self.remove_button_clicked) + song_review_widget.song_remove_button_clicked.connect(self.remove_button_clicked) self.songs_horizontal_layout.addWidget(song_review_widget) self.songs_horizontal_layout.addStretch() self.songs_horizontal_layout.addStretch() diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index af861205e..eb3dc0372 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -42,6 +42,18 @@ class SongReviewWidget(QtGui.QWidget): The remove logic is not implemented here, but a signal is provided when the remove button is clicked. """ + + # Signals have to be class variables and not instance variables. Otherwise + # they are not registered by Qt (missing emit and connect methods are artifacts of this). + # To use SongReviewWidget as a signal parameter one would have to assigning the class + # variable after the class is declared. While this is possible, it also messes Qts meta + # object system up. The result is an + # "Object::connect: Use the SIGNAL macro to bind SongReviewWidget::(QWidget*)" error on + # connect calls. + # That's why we cheat a little and use QWidget instead of SongReviewWidget as parameter. + # While not being entirely correct, it does work. + song_remove_button_clicked = QtCore.pyqtSignal(QtGui.QWidget) + def __init__(self, parent, song): """ ``parent`` @@ -168,4 +180,4 @@ class SongReviewWidget(QtGui.QWidget): """ Signal emitted when the "remove" button is clicked. """ - self.emit(QtCore.SIGNAL(u'song_remove_button_clicked(PyQt_PyObject)'), self) + self.song_remove_button_clicked.emit(self) From 2cf15f3b693737c7f79c966485833a77b9f9b373 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 21 Feb 2013 00:29:54 +0100 Subject: [PATCH 050/235] Add a lot more test functions. --- openlp/plugins/songs/lib/songcompare.py | 40 ++--- .../openlp_plugins/songs/test_lib.py | 146 +++++++++++++++++- 2 files changed, 161 insertions(+), 25 deletions(-) diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py index e230d9f53..f402e1d5f 100644 --- a/openlp/plugins/songs/lib/songcompare.py +++ b/openlp/plugins/songs/lib/songcompare.py @@ -69,15 +69,15 @@ def songs_probably_equal(song1, song2): large = song1.search_lyrics differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() - diff_no_typos = __remove_typos(diff_tuples) - if __length_of_equal_blocks(diff_no_typos) >= min_block_size or \ - __length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: + diff_no_typos = _remove_typos(diff_tuples) + if _length_of_equal_blocks(diff_no_typos) >= min_block_size or \ + _length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: return True else: return False -def __op_length(opcode): +def _op_length(opcode): """ Return the length of a given difference. @@ -87,7 +87,7 @@ def __op_length(opcode): return max(opcode[2] - opcode[1], opcode[4] - opcode[3]) -def __remove_typos(diff): +def _remove_typos(diff): """ Remove typos from a diff set. A typo is a small difference (min_fragment_size). @@ -97,26 +97,26 @@ def __remove_typos(diff): """ # Remove typo at beginning of the string. if len(diff) >= 2: - if diff[0][0] != "equal" and __op_length(diff[0]) <= max_typo_size and \ - __op_length(diff[1]) >= min_fragment_size: + if diff[0][0] != "equal" and _op_length(diff[0]) <= max_typo_size and \ + _op_length(diff[1]) >= min_fragment_size: del diff[0] # Remove typos in the middle of the string. if len(diff) >= 3: for index in range(len(diff) - 3, -1, -1): - if __op_length(diff[index]) >= min_fragment_size and \ - diff[index + 1][0] != "equal" and __op_length(diff[index + 1]) <= max_typo_size and \ - __op_length(diff[index + 2]) >= min_fragment_size: + if _op_length(diff[index]) >= min_fragment_size and \ + diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= max_typo_size and \ + _op_length(diff[index + 2]) >= min_fragment_size: del diff[index + 1] # Remove typo at the end of the string. if len(diff) >= 2: - if __op_length(diff[-2]) >= min_fragment_size and \ - diff[-1][0] != "equal" and __op_length(diff[-1]) <= max_typo_size: + if _op_length(diff[-2]) >= min_fragment_size and \ + diff[-1][0] != "equal" and _op_length(diff[-1]) <= max_typo_size: del diff[-1] # Merge the bordering equal passages that occured by removing differences. for index in range(len(diff) - 2, -1, -1): - if diff[index][0] == "equal" and __op_length(diff[index]) >= min_fragment_size and \ - diff[index + 1][0] == "equal" and __op_length(diff[index + 1]) >= min_fragment_size: + if diff[index][0] == "equal" and _op_length(diff[index]) >= min_fragment_size and \ + diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= min_fragment_size: diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], diff[index + 1][4]) del diff[index + 1] @@ -124,7 +124,7 @@ def __remove_typos(diff): return diff -def __length_of_equal_blocks(diff): +def _length_of_equal_blocks(diff): """ Return the total length of all equal blocks in a diff set. Blocks smaller than min_block_size are not counted. @@ -134,12 +134,12 @@ def __length_of_equal_blocks(diff): """ length = 0 for element in diff: - if element[0] == "equal" and __op_length(element) >= min_block_size: - length += __op_length(element) + if element[0] == "equal" and _op_length(element) >= min_block_size: + length += _op_length(element) return length -def __length_of_longest_equal_block(diff): +def _length_of_longest_equal_block(diff): """ Return the length of the largest equal block in a diff set. @@ -148,6 +148,6 @@ def __length_of_longest_equal_block(diff): """ length = 0 for element in diff: - if element[0] == "equal" and __op_length(element) > length: - length = __op_length(element) + if element[0] == "equal" and _op_length(element) > length: + length = _op_length(element) return length diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index a02563b22..f12e8ce01 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -31,7 +31,8 @@ from unittest import TestCase from mock import MagicMock -from openlp.plugins.songs.lib.songcompare import songs_probably_equal +from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length, \ + _length_of_equal_blocks, _length_of_longest_equal_block class TestLib(TestCase): def setUp(self): @@ -55,6 +56,7 @@ class TestLib(TestCase): self.song1 = MagicMock() self.song2 = MagicMock() + def songs_probably_equal_same_song_test(self): """ Test the songs_probably_equal function with twice the same song. @@ -67,7 +69,7 @@ class TestLib(TestCase): result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. - assert result is True, u'The result should be True' + assert result == True, u'The result should be True' def songs_probably_equal_short_song_test(self): @@ -82,7 +84,7 @@ class TestLib(TestCase): result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. - assert result is True, u'The result should be True' + assert result == True, u'The result should be True' def songs_probably_equal_error_song_test(self): @@ -97,7 +99,7 @@ class TestLib(TestCase): result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be True. - assert result is True, u'The result should be True' + assert result == True, u'The result should be True' def songs_probably_equal_different_song_test(self): @@ -112,4 +114,138 @@ class TestLib(TestCase): result = songs_probably_equal(self.song1, self.song2) #THEN: The result should be False. - assert result is False, u'The result should be False' + assert result == False, u'The result should be False' + + + def remove_typos_beginning_test(self): + """ + Test the _remove_typos function with a typo at the beginning. + """ + #GIVEN: A diffset with a difference at the beginning. + diff = [('replace', 0, 2, 0, 1), ('equal', 2, 11, 1, 10)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There should be no typos at the beginning anymore. + assert len(result) == 1, u'The result should contain only one element.' + assert result[0][0] == 'equal', u'The result should contain an equal element.' + + + def remove_typos_beginning_negated_test(self): + """ + Test the _remove_typos function with a large difference at the beginning. + """ + #GIVEN: A diffset with a large difference at the beginning. + diff = [('replace', 0, 20, 0, 1), ('equal', 20, 29, 1, 10)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There diff should not have changed. + assert result == diff + + + def remove_typos_end_test(self): + """ + Test the _remove_typos function with a typo at the end. + """ + #GIVEN: A diffset with a difference at the end. + diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There should be no typos at the end anymore. + assert len(result) == 1, u'The result should contain only one element.' + assert result[0][0] == 'equal', u'The result should contain an equal element.' + + + def remove_typos_end_negated_test(self): + """ + Test the _remove_typos function with a large difference at the end. + """ + #GIVEN: A diffset with a large difference at the end. + diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 1)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There diff should not have changed. + assert result == diff + + + def remove_typos_middle_test(self): + """ + Test the _remove_typos function with a typo in the middle. + """ + #GIVEN: A diffset with a difference in the middle. + diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11), ('equal', 12, 22, 11, 21)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There should be no typos in the middle anymore. The remaining equals should have been merged. + assert len(result) is 1, u'The result should contain only one element.' + assert result[0][0] == 'equal', u'The result should contain an equal element.' + assert result[0][1] == 0, u'The start indices should be kept.' + assert result[0][2] == 22, u'The stop indices should be kept.' + assert result[0][3] == 0, u'The start indices should be kept.' + assert result[0][4] == 21, u'The stop indices should be kept.' + + + def remove_typos_beginning_negated_test(self): + """ + Test the _remove_typos function with a large difference in the middle. + """ + #GIVEN: A diffset with a large difference in the middle. + diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 11), ('equal', 20, 30, 11, 21)] + + #WHEN: We remove the typos in there. + result = _remove_typos(diff) + + #THEN: There diff should not have changed. + assert result == diff + + + def op_length_test(self): + """ + Test the _op_length function. + """ + #GIVEN: A diff entry. + diff_entry = ('replace', 0, 2, 4, 14) + + #WHEN: We calculate the length of that diff. + result = _op_length(diff_entry) + + #THEN: The maximum length should be returned. + assert result == 10, u'The length should be 10.' + + + def length_of_equal_blocks_test(self): + """ + Test the _length_of_equal_blocks function. + """ + #GIVEN: A diff. + diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 110, 120, 110, 120), \ + ('replace', 120, 200, 120, 200), ('equal', 200, 300, 200, 300)] + + #WHEN: We calculate the length of that diffs equal blocks. + result = _length_of_equal_blocks(diff) + + #THEN: The total length should be returned. Note: Equals smaller 70 are ignored. + assert result == 200, u'The length should be 200.' + + + def length_of_longest_equal_block_test(self): + """ + Test the _length_of_longest_equal_block function. + """ + #GIVEN: A diff. + diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 200, 500, 200, 500)] + + #WHEN: We calculate the length of that diffs longest equal block. + result = _length_of_longest_equal_block(diff) + + #THEN: The total correct length should be returned. + assert result == 300, u'The length should be 300.' \ No newline at end of file From 40de8647cf36dab6c14c8201777cc3a9df44d9b7 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 28 Feb 2013 21:47:59 +0100 Subject: [PATCH 051/235] 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 ab565ae3e9e224cc7e0e8271b66ce9d59e9f6a60 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 28 Feb 2013 22:46:36 +0100 Subject: [PATCH 052/235] Cosmetic comment fixes in testing functions. --- .../openlp_plugins/songs/test_lib.py | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index f12e8ce01..be82d3db0 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -61,14 +61,14 @@ class TestLib(TestCase): """ Test the songs_probably_equal function with twice the same song. """ - #GIVEN: Two equal songs. + # GIVEN: Two equal songs. self.song1.search_lyrics = self.full_lyrics self.song2.search_lyrics = self.full_lyrics - #WHEN: We compare those songs for equality. + # WHEN: We compare those songs for equality. result = songs_probably_equal(self.song1, self.song2) - #THEN: The result should be True. + # THEN: The result should be True. assert result == True, u'The result should be True' @@ -76,14 +76,14 @@ class TestLib(TestCase): """ Test the songs_probably_equal function with a song and a shorter version of the same song. """ - #GIVEN: A song and a short version of the same song. + # GIVEN: A song and a short version of the same song. self.song1.search_lyrics = self.full_lyrics self.song2.search_lyrics = self.short_lyrics - #WHEN: We compare those songs for equality. + # WHEN: We compare those songs for equality. result = songs_probably_equal(self.song1, self.song2) - #THEN: The result should be True. + # THEN: The result should be True. assert result == True, u'The result should be True' @@ -91,14 +91,14 @@ class TestLib(TestCase): """ Test the songs_probably_equal function with a song and a very erroneous version of the same song. """ - #GIVEN: A song and the same song with lots of errors. + # GIVEN: A song and the same song with lots of errors. self.song1.search_lyrics = self.full_lyrics self.song2.search_lyrics = self.error_lyrics - #WHEN: We compare those songs for equality. + # WHEN: We compare those songs for equality. result = songs_probably_equal(self.song1, self.song2) - #THEN: The result should be True. + # THEN: The result should be True. assert result == True, u'The result should be True' @@ -106,14 +106,14 @@ class TestLib(TestCase): """ Test the songs_probably_equal function with two different songs. """ - #GIVEN: Two different songs. + # GIVEN: Two different songs. self.song1.search_lyrics = self.full_lyrics self.song2.search_lyrics = self.different_lyrics - #WHEN: We compare those songs for equality. + # WHEN: We compare those songs for equality. result = songs_probably_equal(self.song1, self.song2) - #THEN: The result should be False. + # THEN: The result should be False. assert result == False, u'The result should be False' @@ -121,13 +121,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a typo at the beginning. """ - #GIVEN: A diffset with a difference at the beginning. + # GIVEN: A diffset with a difference at the beginning. diff = [('replace', 0, 2, 0, 1), ('equal', 2, 11, 1, 10)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There should be no typos at the beginning anymore. + # THEN: There should be no typos at the beginning anymore. assert len(result) == 1, u'The result should contain only one element.' assert result[0][0] == 'equal', u'The result should contain an equal element.' @@ -136,13 +136,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a large difference at the beginning. """ - #GIVEN: A diffset with a large difference at the beginning. + # GIVEN: A diffset with a large difference at the beginning. diff = [('replace', 0, 20, 0, 1), ('equal', 20, 29, 1, 10)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There diff should not have changed. + # THEN: There diff should not have changed. assert result == diff @@ -150,13 +150,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a typo at the end. """ - #GIVEN: A diffset with a difference at the end. + # GIVEN: A diffset with a difference at the end. diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There should be no typos at the end anymore. + # THEN: There should be no typos at the end anymore. assert len(result) == 1, u'The result should contain only one element.' assert result[0][0] == 'equal', u'The result should contain an equal element.' @@ -165,13 +165,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a large difference at the end. """ - #GIVEN: A diffset with a large difference at the end. + # GIVEN: A diffset with a large difference at the end. diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 1)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There diff should not have changed. + # THEN: There diff should not have changed. assert result == diff @@ -179,13 +179,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a typo in the middle. """ - #GIVEN: A diffset with a difference in the middle. + # GIVEN: A diffset with a difference in the middle. diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11), ('equal', 12, 22, 11, 21)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There should be no typos in the middle anymore. The remaining equals should have been merged. + # THEN: There should be no typos in the middle anymore. The remaining equals should have been merged. assert len(result) is 1, u'The result should contain only one element.' assert result[0][0] == 'equal', u'The result should contain an equal element.' assert result[0][1] == 0, u'The start indices should be kept.' @@ -198,13 +198,13 @@ class TestLib(TestCase): """ Test the _remove_typos function with a large difference in the middle. """ - #GIVEN: A diffset with a large difference in the middle. + # GIVEN: A diffset with a large difference in the middle. diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 11), ('equal', 20, 30, 11, 21)] - #WHEN: We remove the typos in there. + # WHEN: We remove the typos in there. result = _remove_typos(diff) - #THEN: There diff should not have changed. + # THEN: There diff should not have changed. assert result == diff @@ -212,13 +212,13 @@ class TestLib(TestCase): """ Test the _op_length function. """ - #GIVEN: A diff entry. + # GIVEN: A diff entry. diff_entry = ('replace', 0, 2, 4, 14) - #WHEN: We calculate the length of that diff. + # WHEN: We calculate the length of that diff. result = _op_length(diff_entry) - #THEN: The maximum length should be returned. + # THEN: The maximum length should be returned. assert result == 10, u'The length should be 10.' @@ -226,14 +226,14 @@ class TestLib(TestCase): """ Test the _length_of_equal_blocks function. """ - #GIVEN: A diff. + # GIVEN: A diff. diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 110, 120, 110, 120), \ ('replace', 120, 200, 120, 200), ('equal', 200, 300, 200, 300)] - #WHEN: We calculate the length of that diffs equal blocks. + # WHEN: We calculate the length of that diffs equal blocks. result = _length_of_equal_blocks(diff) - #THEN: The total length should be returned. Note: Equals smaller 70 are ignored. + # THEN: The total length should be returned. Note: Equals smaller 70 are ignored. assert result == 200, u'The length should be 200.' @@ -241,11 +241,11 @@ class TestLib(TestCase): """ Test the _length_of_longest_equal_block function. """ - #GIVEN: A diff. + # GIVEN: A diff. diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 200, 500, 200, 500)] - #WHEN: We calculate the length of that diffs longest equal block. + # WHEN: We calculate the length of that diffs longest equal block. result = _length_of_longest_equal_block(diff) - #THEN: The total correct length should be returned. + # dTHEN: The total correct length should be returned. assert result == 300, u'The length should be 300.' \ No newline at end of file From 2f21ea2f460276d526f85eb066ce6a005369788d Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 28 Feb 2013 23:01:05 +0100 Subject: [PATCH 053/235] Add can_shortcuts=True to menu entry action. --- openlp/plugins/songs/songsplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 237b898ba..713e301de 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -153,7 +153,7 @@ class SongsPlugin(Plugin): text=translate('SongsPlugin', 'Find &Duplicate Songs'), statustip=translate('SongsPlugin', 'Find and remove duplicate songs in the song database.'), - visible=False, triggers=self.on_tools_find_duplicates_triggered) + visible=False, triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True) tools_menu.addAction(self.tools_find_duplicates) def onToolsReindexItemTriggered(self): From 8f5dd8f649cadb04c78ace1d6eb24b84cf35761c Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 28 Feb 2013 23:04:03 +0100 Subject: [PATCH 054/235] Format some constants correctly. --- openlp/plugins/songs/lib/songcompare.py | 28 ++++++++++++------------- openlp/plugins/songs/songsplugin.py | 3 +-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py index f402e1d5f..543156314 100644 --- a/openlp/plugins/songs/lib/songcompare.py +++ b/openlp/plugins/songs/lib/songcompare.py @@ -46,9 +46,9 @@ Finally two conditions can qualify a song tuple to be a duplicate: import difflib -min_fragment_size = 5 -min_block_size = 70 -max_typo_size = 3 +MIN_FRAGMENT_SIZE = 5 +MIN_BLOCK_SIZE = 70 +MAX_TYPO_SIZE = 3 def songs_probably_equal(song1, song2): @@ -70,7 +70,7 @@ def songs_probably_equal(song1, song2): differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() diff_no_typos = _remove_typos(diff_tuples) - if _length_of_equal_blocks(diff_no_typos) >= min_block_size or \ + if _length_of_equal_blocks(diff_no_typos) >= MIN_BLOCK_SIZE or \ _length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: return True else: @@ -97,26 +97,26 @@ def _remove_typos(diff): """ # Remove typo at beginning of the string. if len(diff) >= 2: - if diff[0][0] != "equal" and _op_length(diff[0]) <= max_typo_size and \ - _op_length(diff[1]) >= min_fragment_size: + if diff[0][0] != "equal" and _op_length(diff[0]) <= MAX_TYPO_SIZE and \ + _op_length(diff[1]) >= MIN_FRAGMENT_SIZE: del diff[0] # Remove typos in the middle of the string. if len(diff) >= 3: for index in range(len(diff) - 3, -1, -1): - if _op_length(diff[index]) >= min_fragment_size and \ - diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= max_typo_size and \ - _op_length(diff[index + 2]) >= min_fragment_size: + if _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \ + diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= MAX_TYPO_SIZE and \ + _op_length(diff[index + 2]) >= MIN_FRAGMENT_SIZE: del diff[index + 1] # Remove typo at the end of the string. if len(diff) >= 2: - if _op_length(diff[-2]) >= min_fragment_size and \ - diff[-1][0] != "equal" and _op_length(diff[-1]) <= max_typo_size: + if _op_length(diff[-2]) >= MIN_FRAGMENT_SIZE and \ + diff[-1][0] != "equal" and _op_length(diff[-1]) <= MAX_TYPO_SIZE: del diff[-1] # Merge the bordering equal passages that occured by removing differences. for index in range(len(diff) - 2, -1, -1): - if diff[index][0] == "equal" and _op_length(diff[index]) >= min_fragment_size and \ - diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= min_fragment_size: + if diff[index][0] == "equal" and _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \ + diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= MIN_FRAGMENT_SIZE: diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3], diff[index + 1][4]) del diff[index + 1] @@ -134,7 +134,7 @@ def _length_of_equal_blocks(diff): """ length = 0 for element in diff: - if element[0] == "equal" and _op_length(element) >= min_block_size: + if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE: length += _op_length(element) return length diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 713e301de..7acf0bcef 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -48,8 +48,7 @@ from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.mediaitem import SongSearch from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport -from openlp.plugins.songs.forms.duplicatesongremovalform import \ - DuplicateSongRemovalForm +from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm log = logging.getLogger(__name__) __default_settings__ = { From 8de486f869efb862d3821f17559e1bb7e096d2ca Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 28 Feb 2013 23:20:48 +0100 Subject: [PATCH 055/235] Simplify (and slightly speed up) song comparison logic by inlining two functions. --- openlp/plugins/songs/lib/songcompare.py | 53 +++++++------------ .../openlp_plugins/songs/test_lib.py | 32 +---------- 2 files changed, 20 insertions(+), 65 deletions(-) diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py index 543156314..a98e61380 100644 --- a/openlp/plugins/songs/lib/songcompare.py +++ b/openlp/plugins/songs/lib/songcompare.py @@ -70,11 +70,25 @@ def songs_probably_equal(song1, song2): differ = difflib.SequenceMatcher(a=large, b=small) diff_tuples = differ.get_opcodes() diff_no_typos = _remove_typos(diff_tuples) - if _length_of_equal_blocks(diff_no_typos) >= MIN_BLOCK_SIZE or \ - _length_of_longest_equal_block(diff_no_typos) > len(small) * 2 / 3: - return True - else: - return False + # Check 1: Similarity based on the absolute length of equal parts. + # Calculate the total length of all equal blocks of the set. + # Blocks smaller than min_block_size are not counted. + length_of_equal_blocks = 0 + for element in diff_no_typos: + if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE: + length_of_equal_blocks += _op_length(element) + if length_of_equal_blocks >= MIN_BLOCK_SIZE: + return True + # Check 2: Similarity based on the relative length of the longest equal block. + # Calculate the length of the largest equal block of the diff set. + length_of_longest_equal_block = 0 + for element in diff_no_typos: + if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block: + length_of_longest_equal_block = _op_length(element) + if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 / 3: + return True + # Both checks failed. We assume the songs are not equal. + return False def _op_length(opcode): @@ -122,32 +136,3 @@ def _remove_typos(diff): del diff[index + 1] return diff - - -def _length_of_equal_blocks(diff): - """ - Return the total length of all equal blocks in a diff set. - Blocks smaller than min_block_size are not counted. - - ``diff`` - The diff set to return the length for. - """ - length = 0 - for element in diff: - if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE: - length += _op_length(element) - return length - - -def _length_of_longest_equal_block(diff): - """ - Return the length of the largest equal block in a diff set. - - ``diff`` - The diff set to return the length for. - """ - length = 0 - for element in diff: - if element[0] == "equal" and _op_length(element) > length: - length = _op_length(element) - return length diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index be82d3db0..b79f51132 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -31,8 +31,7 @@ from unittest import TestCase from mock import MagicMock -from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length, \ - _length_of_equal_blocks, _length_of_longest_equal_block +from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length class TestLib(TestCase): def setUp(self): @@ -220,32 +219,3 @@ class TestLib(TestCase): # THEN: The maximum length should be returned. assert result == 10, u'The length should be 10.' - - - def length_of_equal_blocks_test(self): - """ - Test the _length_of_equal_blocks function. - """ - # GIVEN: A diff. - diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 110, 120, 110, 120), \ - ('replace', 120, 200, 120, 200), ('equal', 200, 300, 200, 300)] - - # WHEN: We calculate the length of that diffs equal blocks. - result = _length_of_equal_blocks(diff) - - # THEN: The total length should be returned. Note: Equals smaller 70 are ignored. - assert result == 200, u'The length should be 200.' - - - def length_of_longest_equal_block_test(self): - """ - Test the _length_of_longest_equal_block function. - """ - # GIVEN: A diff. - diff = [('equal', 0, 100, 0, 100), ('replace', 100, 110, 100, 110), ('equal', 200, 500, 200, 500)] - - # WHEN: We calculate the length of that diffs longest equal block. - result = _length_of_longest_equal_block(diff) - - # dTHEN: The total correct length should be returned. - assert result == 300, u'The length should be 300.' \ No newline at end of file From 1de79a0ec20c9b453193ba94a69afc0208119129 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 28 Feb 2013 23:46:55 +0100 Subject: [PATCH 056/235] Fix thinko: Three tests always evaluated to true by comparing a list with itself. --- tests/functional/openlp_plugins/songs/test_lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_lib.py b/tests/functional/openlp_plugins/songs/test_lib.py index b79f51132..a0898479b 100644 --- a/tests/functional/openlp_plugins/songs/test_lib.py +++ b/tests/functional/openlp_plugins/songs/test_lib.py @@ -139,7 +139,7 @@ class TestLib(TestCase): diff = [('replace', 0, 20, 0, 1), ('equal', 20, 29, 1, 10)] # WHEN: We remove the typos in there. - result = _remove_typos(diff) + result = _remove_typos(list(diff)) # THEN: There diff should not have changed. assert result == diff @@ -168,7 +168,7 @@ class TestLib(TestCase): diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 1)] # WHEN: We remove the typos in there. - result = _remove_typos(diff) + result = _remove_typos(list(diff)) # THEN: There diff should not have changed. assert result == diff @@ -201,7 +201,7 @@ class TestLib(TestCase): diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 11), ('equal', 20, 30, 11, 21)] # WHEN: We remove the typos in there. - result = _remove_typos(diff) + result = _remove_typos(list(diff)) # THEN: There diff should not have changed. assert result == diff From f5774c48ecc82e3fa22ee29a5a23442670559c38 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 4 Mar 2013 21:48:28 +0100 Subject: [PATCH 057/235] Do not show cancel button on finish pages. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 4642d6fdb..b87e55370 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -131,7 +131,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): u'review every potential duplicate song before it is deleted. So no songs will be deleted without your ' u'explicit approval.')) self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) - self.searching_page.setSubTitle(translate(u'Wizard', u'The song database is searched for double songs.')) + self.searching_page.setSubTitle(translate(u'Wizard', u'Please wait while your songs database is searched.')) self.update_review_counter_text() self.review_page.setSubTitle(translate(u'Wizard', u'Here you can decide which songs to remove and which ones to keep.')) @@ -186,6 +186,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() + self.button(QtGui.QWizard.CancelButton).hide() QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'), translate(u'Wizard', u'No duplicate songs have been found in the database.'), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) @@ -340,7 +341,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.FinishButton).show() self.button(QtGui.QWizard.FinishButton).setEnabled(True) self.button(QtGui.QWizard.NextButton).hide() - + self.button(QtGui.QWizard.CancelButton).hide() + def _get_main_window(self): """ Adds the main window to the class dynamically. From b740f54352bb154dabe715102099f4dfc943dbbe Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 19:52:10 +0100 Subject: [PATCH 058/235] Change verse widget to a QTableView. And increase songwidget width. --- .../plugins/songs/forms/songreviewwidget.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index eb3dc0372..a853c8b8b 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -73,8 +73,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_vertical_layout.setObjectName(u'song_vertical_layout') self.song_group_box = QtGui.QGroupBox(self) self.song_group_box.setObjectName(u'song_group_box') - self.song_group_box.setMinimumWidth(300) - self.song_group_box.setMaximumWidth(300) + self.song_group_box.setFixedWidth(400) self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) self.song_group_box_layout.setObjectName(u'song_group_box_layout') self.song_info_form_layout = QtGui.QFormLayout() @@ -143,20 +142,34 @@ class SongReviewWidget(QtGui.QWidget): self.song_verse_order_content.setText(self.song.verse_order) self.song_verse_order_content.setWordWrap(True) self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content) - # Add verses widget. self.song_group_box_layout.addLayout(self.song_info_form_layout) - self.song_info_verse_group_box = QtGui.QGroupBox(self.song_group_box) - self.song_info_verse_group_box.setObjectName(u'song_info_verse_group_box') - self.song_info_verse_group_box_layout = QtGui.QFormLayout(self.song_info_verse_group_box) + # Add verses widget. + self.song_info_verse_list_widget = QtGui.QTableWidget(self.song_group_box) + self.song_info_verse_list_widget.setColumnCount(1) + self.song_info_verse_list_widget.horizontalHeader().setVisible(False) + self.song_info_verse_list_widget.setObjectName(u'song_info_verse_list_widget') + self.song_info_verse_list_widget.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + self.song_info_verse_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.song_info_verse_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.song_info_verse_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.song_info_verse_list_widget.setAlternatingRowColors(True) song_xml = SongXML() verses = song_xml.get_verses(self.song.lyrics) - for verse in verses: - verse_marker = verse[0]['type'] + verse[0]['label'] - verse_label = QtGui.QLabel(self.song_info_verse_group_box) - verse_label.setText(verse[1]) - verse_label.setWordWrap(True) - self.song_info_verse_group_box_layout.addRow(verse_marker, verse_label) - self.song_group_box_layout.addWidget(self.song_info_verse_group_box) + self.song_info_verse_list_widget.setRowCount(len(verses)) + song_tags = [] + for verse_number, verse in enumerate(verses): + item = QtGui.QTableWidgetItem() + item.setText(verse[1]) + self.song_info_verse_list_widget.setItem(verse_number, 0, item) + song_tags.append(unicode(verse[0]['type'] + verse[0]['label'])) + self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags) + # Resize table fields to content and table to columns + self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width()) + self.song_info_verse_list_widget.resizeRowsToContents() + # The 6 is a trial and error value to just remove the scrollbar. + # TODO: Might be a different value with different skins. + self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() + 6) + self.song_group_box_layout.addWidget(self.song_info_verse_list_widget) self.song_group_box_layout.addStretch() self.song_vertical_layout.addWidget(self.song_group_box) self.song_remove_button = QtGui.QPushButton(self) @@ -174,8 +187,6 @@ class SongReviewWidget(QtGui.QWidget): self.song_copyright_label.setText(u'Copyright:') self.song_comments_label.setText(u'Comments:') self.song_authors_label.setText(u'Authors:') - self.song_info_verse_group_box.setTitle(u'Verses') - def on_remove_button_clicked(self): """ Signal emitted when the "remove" button is clicked. From 955a1a3be47eb3e63273a722866102ea829a4996 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 20:49:11 +0100 Subject: [PATCH 059/235] Document a magic number. --- openlp/plugins/songs/forms/songreviewwidget.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index a853c8b8b..ec7ff9f75 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -34,6 +34,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import build_icon from openlp.plugins.songs.lib.xml import SongXML + class SongReviewWidget(QtGui.QWidget): """ A widget representing a song on the duplicate song review page. @@ -166,9 +167,13 @@ class SongReviewWidget(QtGui.QWidget): # Resize table fields to content and table to columns self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width()) self.song_info_verse_list_widget.resizeRowsToContents() - # The 6 is a trial and error value to just remove the scrollbar. - # TODO: Might be a different value with different skins. - self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() + 6) + # The 6 is a trial and error value since verticalHeader().length() + offset() is a little bit to small. + # It seems there is no clean way to determine the real height of the table contents. + # The "correct" value slightly fluctuates depending on the theme used, in the worst case + # Some pixels are missing at the bottom of the table, but all themes I tried still allowed + # to read the last verse line, so I'll just leave it at that. + self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() + + self.song_info_verse_list_widget.verticalHeader().offset() + 6) self.song_group_box_layout.addWidget(self.song_info_verse_list_widget) self.song_group_box_layout.addStretch() self.song_vertical_layout.addWidget(self.song_group_box) @@ -187,6 +192,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_copyright_label.setText(u'Copyright:') self.song_comments_label.setText(u'Comments:') self.song_authors_label.setText(u'Authors:') + def on_remove_button_clicked(self): """ Signal emitted when the "remove" button is clicked. From daca6135e955aeea1b8303b58b83f22de46aeaff Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 20:54:03 +0100 Subject: [PATCH 060/235] Readd the scrollarea border on the review page and remove the then unnecessary stylesheets. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index b87e55370..e1888e90a 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -100,14 +100,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songs_horizontal_scroll_area.setObjectName(u'songs_horizontal_scroll_area') self.songs_horizontal_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.songs_horizontal_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songs_horizontal_scroll_area.setFrameStyle(QtGui.QFrame.NoFrame) self.songs_horizontal_scroll_area.setWidgetResizable(True) - self.songs_horizontal_scroll_area.setStyleSheet( - u'QScrollArea#songs_horizontal_scroll_area {background-color:transparent;}') self.songs_horizontal_songs_widget = QtGui.QWidget(self.songs_horizontal_scroll_area) self.songs_horizontal_songs_widget.setObjectName(u'songs_horizontal_songs_widget') - self.songs_horizontal_songs_widget.setStyleSheet( - u'QWidget#songs_horizontal_songs_widget {background-color:transparent;}') self.songs_horizontal_layout = QtGui.QHBoxLayout(self.songs_horizontal_songs_widget) self.songs_horizontal_layout.setObjectName(u'songs_horizontal_layout') self.songs_horizontal_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) From aced0516b07090c8568b2be5d5864753aacd539a Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 23:18:35 +0100 Subject: [PATCH 061/235] Remove broken quad-stretch and replace with two normal stretches with factor. --- .../plugins/songs/forms/duplicatesongremovalform.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index e1888e90a..4037451fd 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -286,10 +286,10 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.songs_horizontal_layout.removeWidget(song_review_widget) song_review_widget.setParent(None) # Check if we only have one duplicate left: - # 4 stretches + 1 SongReviewWidget = 5 - # The SongReviewWidget is then at position 2. + # 2 stretches + 1 SongReviewWidget = 3 + # The SongReviewWidget is then at position 1. if len(self.duplicate_song_list[-1]) == 1: - self.songs_horizontal_layout.itemAt(2).widget().song_remove_button.setEnabled(False) + self.songs_horizontal_layout.itemAt(1).widget().song_remove_button.setEnabled(False) def proceed_to_next_review(self): """ @@ -322,15 +322,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.update_review_counter_text() # Add song elements to the UI. if len(self.duplicate_song_list) > 0: - # A stretch doesn't seem to stretch endlessly, so I add two to get enough stetch for 1400x1050. - self.songs_horizontal_layout.addStretch() - self.songs_horizontal_layout.addStretch() + self.songs_horizontal_layout.addStretch(1) for duplicate in self.duplicate_song_list[-1]: song_review_widget = SongReviewWidget(self.review_page, duplicate) song_review_widget.song_remove_button_clicked.connect(self.remove_button_clicked) self.songs_horizontal_layout.addWidget(song_review_widget) - self.songs_horizontal_layout.addStretch() - self.songs_horizontal_layout.addStretch() + self.songs_horizontal_layout.addStretch(1) # Change next button to finish button on last review. if len(self.duplicate_song_list) == 1: self.button(QtGui.QWizard.FinishButton).show() From 001dd40c6b4f2b1dbb9892d5879e32604149411e Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 23:20:09 +0100 Subject: [PATCH 062/235] Minor string change. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 4037451fd..a4efedbe0 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -126,7 +126,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): u'review every potential duplicate song before it is deleted. So no songs will be deleted without your ' u'explicit approval.')) self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.')) - self.searching_page.setSubTitle(translate(u'Wizard', u'Please wait while your songs database is searched.')) + self.searching_page.setSubTitle(translate(u'Wizard', u'Please wait while your songs database is analyzed.')) self.update_review_counter_text() self.review_page.setSubTitle(translate(u'Wizard', u'Here you can decide which songs to remove and which ones to keep.')) From c3d16aa4b945370cc6cf8f658721837904e7c221 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 5 Mar 2013 23:25:25 +0100 Subject: [PATCH 063/235] Hide next button during search. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index a4efedbe0..929d6d98a 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -148,6 +148,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): # Hide back button. self.button(QtGui.QWizard.BackButton).hide() if page_id == self.searching_page_id: + self.button(QtGui.QWizard.NextButton).hide() # Search duplicate songs. max_songs = self.plugin.manager.get_object_count(Song) if max_songs == 0 or max_songs == 1: @@ -171,6 +172,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.review_total_count = len(self.duplicate_song_list) if self.review_total_count == 0: self.notify_no_duplicates() + else: + self.button(QtGui.QWizard.NextButton).show() elif page_id == self.review_page_id: self.process_current_duplicate_entry() From f1aadde13cd30ecf89afb8d8bd07ad9f4e0b5210 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 6 Mar 2013 21:53:29 +0000 Subject: [PATCH 064/235] 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 065/235] 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 066/235] 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 067/235] 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 068/235] 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 069/235] 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 070/235] 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 071/235] 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 072/235] 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 073/235] 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 074/235] 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 df1faa2a8dfc71f5c7dac57aa0b197c4c80e1343 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Mon, 25 Mar 2013 21:17:46 +0100 Subject: [PATCH 075/235] - Fixed traceback on canceling the 'choose group' dialog - Clear the 'new group' lineedit in the 'choose group' dialog --- openlp/plugins/images/forms/choosegroupform.py | 1 + openlp/plugins/images/lib/mediaitem.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openlp/plugins/images/forms/choosegroupform.py b/openlp/plugins/images/forms/choosegroupform.py index bbb57255c..f5478d014 100644 --- a/openlp/plugins/images/forms/choosegroupform.py +++ b/openlp/plugins/images/forms/choosegroupform.py @@ -50,6 +50,7 @@ class ChooseGroupForm(QtGui.QDialog, Ui_ChooseGroupDialog): ``selected_group`` The ID of the group that should be selected by default when showing the dialog """ + self.new_group_edit.clear() if selected_group is not None: for i in range(self.group_combobox.count()): if self.group_combobox.itemData(i) == selected_group: diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 1b2fe512e..ceb67f74b 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -394,6 +394,7 @@ class ImageMediaItem(MediaManagerItem): ``initial_load`` When set to False, the busy cursor and progressbar will be shown while loading images """ + parent_group = None if target_group is None: # Find out if a group must be pre-selected preselect_group = None From 401f5ac2be3b087abfecf4bf6fbbcbca06289d7e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 08:55:05 +0000 Subject: [PATCH 076/235] 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 44547769c637a46210f488ce751b1968a84d5bd4 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 26 Mar 2013 10:47:05 +0100 Subject: [PATCH 077/235] - Fixed traceback on adding multiple image groups to the service in one go - Cleaned up obsolete line in imagetab --- openlp/core/lib/mediamanageritem.py | 2 +- openlp/plugins/images/lib/imagetab.py | 1 - openlp/plugins/images/lib/mediaitem.py | 117 ++++++++++++++----------- 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index e261bfe59..960708620 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -464,7 +464,7 @@ class MediaManagerItem(QtGui.QWidget): translate('OpenLP.MediaManagerItem', 'You must select one or more items to preview.')) else: log.debug(u'%s Preview requested', self.plugin.name) - service_item = self.build_service_item() + service_item = self.build_service_item(context=ServiceItemContext.Preview) if service_item: service_item.from_plugin = True self.preview_controller.add_service_item(service_item) diff --git a/openlp/plugins/images/lib/imagetab.py b/openlp/plugins/images/lib/imagetab.py index 83d015cb8..b43b19097 100644 --- a/openlp/plugins/images/lib/imagetab.py +++ b/openlp/plugins/images/lib/imagetab.py @@ -32,7 +32,6 @@ from PyQt4 import QtGui from openlp.core.lib import Registry, SettingsTab, Settings, UiStrings, translate - class ImageTab(SettingsTab): """ ImageTab is the images settings tab in the settings dialog. diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index ceb67f74b..9bdd1dd11 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -32,7 +32,7 @@ import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, ServiceItemContext, Settings, \ +from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, ServiceItem, ServiceItemContext, Settings, \ 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 @@ -538,63 +538,74 @@ class ImageMediaItem(MediaManagerItem): """ background = QtGui.QColor(Settings().value(self.settings_section + u'/background color')) if item: - items = [item] + new_items = [item] else: - items = self.list_view.selectedItems() - if not items: + new_items = self.list_view.selectedItems() + if not new_items: return False - # Determine service item title - if isinstance(items[0].data(0, QtCore.Qt.UserRole), ImageGroups): - service_item.title = items[0].text(0) - else: - service_item.title = unicode(self.plugin.name_strings[u'plural']) - service_item.add_capability(ItemCapabilities.CanMaintain) - service_item.add_capability(ItemCapabilities.CanPreview) - service_item.add_capability(ItemCapabilities.CanLoop) - service_item.add_capability(ItemCapabilities.CanAppend) - # force a nonexistent theme - service_item.theme = -1 - missing_items = [] - missing_items_filenames = [] - # Expand groups to images - for bitem in items: + for bitem in new_items: + new_service_item = ServiceItem(self.plugin) + new_service_item.add_icon(self.plugin.icon_path) + # Determine service item title + if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups): + new_service_item.title = bitem.text(0) + else: + new_service_item.title = unicode(self.plugin.name_strings[u'plural']) + new_service_item.add_capability(ItemCapabilities.CanMaintain) + new_service_item.add_capability(ItemCapabilities.CanPreview) + new_service_item.add_capability(ItemCapabilities.CanLoop) + new_service_item.add_capability(ItemCapabilities.CanAppend) + # force a nonexistent theme + new_service_item.theme = -1 + sub_images = [] + missing_items = [] + missing_items_filenames = [] + # Expand groups to images if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None: for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): - items.append(bitem.child(index)) - items.remove(bitem) - # Don't try to display empty groups - if not items: - return False - # Find missing files - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename - if not os.path.exists(filename): - missing_items.append(bitem) - missing_items_filenames.append(filename) - for item in missing_items: - items.remove(item) - # We cannot continue, as all images do not exist. - if not items: - if not remote: - critical_error_message_box( - translate('ImagePlugin.MediaItem', 'Missing Image(s)'), - translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s') % - u'\n'.join(missing_items_filenames)) - return False - # We have missing as well as existing images. We ask what to do. - elif missing_items and QtGui.QMessageBox.question(self, - translate('ImagePlugin.MediaItem', 'Missing Image(s)'), - translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' - 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: - return False - # Continue with the existing images. - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename - name = os.path.split(filename)[1] - service_item.add_from_image(filename, name, background) - return True + sub_images.append(bitem.child(index)) + # Don't try to display empty groups + if not sub_images: + return False + # Find missing files + for bitem in sub_images: + filename = bitem.data(0, QtCore.Qt.UserRole).filename + if not os.path.exists(filename): + missing_items.append(bitem) + missing_items_filenames.append(filename) + for item in missing_items: + sub_images.remove(item) + # We cannot continue, as all images do not exist. + if not sub_images: + if not remote: + critical_error_message_box( + translate('ImagePlugin.MediaItem', 'Missing Image(s)'), + translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s') % + u'\n'.join(missing_items_filenames)) + return False + # We have missing as well as existing images. We ask what to do. + elif missing_items and QtGui.QMessageBox.question(self, + translate('ImagePlugin.MediaItem', 'Missing Image(s)'), + translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' + 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == \ + QtGui.QMessageBox.No: + return False + # Continue with the existing images. + for sub_image in sub_images: + filename = sub_image.data(0, QtCore.Qt.UserRole).filename + name = os.path.split(filename)[1] + new_service_item.add_from_image(filename, name, background) + # Add the service item to the correct controller + if context == ServiceItemContext.Service: + self.service_manager.add_service_item(new_service_item) + elif context == ServiceItemContext.Preview: + self.preview_controller.add_service_item(new_service_item) + elif context == ServiceItemContext.Live: + self.live_controller.add_service_item(new_service_item) + # Return False because we added the service item ourselves + return False def check_group_exists(self, new_group): """ From 6b032d52e554a2080ef9fb3127df1d2c0ccd5bf8 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 26 Mar 2013 11:32:22 +0100 Subject: [PATCH 078/235] - Fixed adding single images --- openlp/plugins/images/lib/mediaitem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 163d7094e..75c6a1216 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -565,6 +565,8 @@ class ImageMediaItem(MediaManagerItem): for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): sub_images.append(bitem.child(index)) + if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): + sub_images.append(bitem) # Don't try to display empty groups if not sub_images: return False From 451189b22202a5075dcadcf714e4d3c6b9067630 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 26 Mar 2013 11:39:32 +0000 Subject: [PATCH 079/235] 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 080/235] 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 4831fb29fa13460e250c72e0d905fdff1486a114 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 26 Mar 2013 22:31:46 +0100 Subject: [PATCH 081/235] Translate verse tags. --- openlp/plugins/songs/forms/songreviewwidget.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index ec7ff9f75..0557a35b5 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -32,6 +32,7 @@ A widget representing a song in the duplicate song removal wizard review page. from PyQt4 import QtCore, QtGui from openlp.core.lib import build_icon +from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib.xml import SongXML @@ -162,7 +163,19 @@ class SongReviewWidget(QtGui.QWidget): item = QtGui.QTableWidgetItem() item.setText(verse[1]) self.song_info_verse_list_widget.setItem(verse_number, 0, item) - song_tags.append(unicode(verse[0]['type'] + verse[0]['label'])) + + # We cannot use from_loose_input() here, because database + # is supposed to contain English lowercase singlechar tags. + verse_tag = verse[0][u'type'] + verse_index = None + if len(verse_tag) > 1: + verse_index = VerseType.from_translated_string(verse_tag) + if verse_index is None: + verse_index = VerseType.from_string(verse_tag, None) + if verse_index is None: + verse_index = VerseType.from_tag(verse_tag) + verse_tag = VerseType.translated_tags[verse_index].upper() + song_tags.append(unicode(verse_tag + verse[0]['label'])) self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags) # Resize table fields to content and table to columns self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width()) From ab011a22c195ec0781e57b38f97de3ab3538d3e1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Thu, 28 Mar 2013 14:45:47 +0000 Subject: [PATCH 082/235] 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 083/235] 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 084/235] 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 085/235] 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 086/235] 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 087/235] 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 088/235] 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 089/235] 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 090/235] 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 091/235] 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 28f8e31bf0223b206e94f622e791f0069a28275e Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Fri, 29 Mar 2013 13:00:06 +0100 Subject: [PATCH 092/235] Added a simple test for onResetClick() --- .../functional/openlp_plugins/images/test_lib.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index a355e956b..6349745c2 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -23,6 +23,7 @@ class TestImageMediaItem(TestCase): Registry.create() Registry().register(u'service_list', MagicMock()) Registry().register(u'main_window', self.mocked_main_window) + Registry().register(u'live_controller', MagicMock()) mocked_parent = MagicMock() mocked_plugin = MagicMock() with patch(u'openlp.plugins.images.lib.mediaitem.ImageMediaItem.__init__') as mocked_init: @@ -110,3 +111,17 @@ class TestImageMediaItem(TestCase): # THEN: loadFullList() should not have been called assert self.media_item.manager.save_object.call_count == 2, \ u'loadFullList() should have been called only once' + + def on_reset_click_test(self): + """ + Test that onResetClick() actually resets the background + """ + # GIVEN: + self.media_item.resetAction = MagicMock() + + # WHEN: + self.media_item.onResetClick() + + # THEN: + self.media_item.resetAction.setVisible.assert_called_with(False) + self.media_item.live_controller.display.reset_image.assert_called_with() From e8dbbdfc896a68e9c353ef707254b8019e0c0ba2 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Fri, 29 Mar 2013 15:15:16 +0100 Subject: [PATCH 093/235] Added a test for recursively_delete_group() --- .../openlp_plugins/images/test_lib.py | 59 ++++++++++++++++-- tests/interfaces/openlp_plugins/__init__.pyc | Bin 184 -> 0 bytes 2 files changed, 55 insertions(+), 4 deletions(-) delete mode 100644 tests/interfaces/openlp_plugins/__init__.pyc diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index 6349745c2..bdfe29373 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -9,7 +9,7 @@ from unittest import TestCase from mock import MagicMock, patch from openlp.core.lib import Registry -from openlp.plugins.images.lib.db import ImageFilenames +from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups from openlp.plugins.images.lib.mediaitem import ImageMediaItem @@ -116,12 +116,63 @@ class TestImageMediaItem(TestCase): """ Test that onResetClick() actually resets the background """ - # GIVEN: + # GIVEN: A mocked version of resetAction self.media_item.resetAction = MagicMock() - # WHEN: + # WHEN: onResetClick is called self.media_item.onResetClick() - # THEN: + # THEN: the resetAction should be set visible, and the image should be reset self.media_item.resetAction.setVisible.assert_called_with(False) self.media_item.live_controller.display.reset_image.assert_called_with() + + def recursively_delete_group_side_effect(*args, **kwargs): + """ + Side effect method that creates custom retun values for the recursively_delete_group method + """ + if args[1] == ImageFilenames and args[2]: + # Create some fake objects that should be removed + returned_object1 = ImageFilenames() + returned_object1.id = 1 + returned_object1.filename = u'/tmp/test_file_1.jpg' + returned_object2 = ImageFilenames() + returned_object2.id = 2 + returned_object2.filename = u'/tmp/test_file_2.jpg' + returned_object3 = ImageFilenames() + returned_object3.id = 3 + returned_object3.filename = u'/tmp/test_file_3.jpg' + return [returned_object1, returned_object2, returned_object3] + if args[1] == ImageGroups and args[2]: + # Change the parent_id that is matched so we don't get into an endless loop + ImageGroups.parent_id = 0 + # Create a fake group that will be used in the next run + returned_object1 = ImageGroups() + returned_object1.id = 1 + return [returned_object1] + return [] + + def recursively_delete_group_test(self): + """ + Test that recursively_delete_group() works + """ + # GIVEN: An ImageGroups object and mocked functions + with patch(u'openlp.core.utils.delete_file') as mocked_delete_file: + ImageFilenames.group_id = 1 + ImageGroups.parent_id = 1 + self.media_item.manager = MagicMock() + self.media_item.manager.get_all_objects.side_effect = self.recursively_delete_group_side_effect + self.media_item.servicePath = "" + test_group = ImageGroups() + test_group.id = 1 + + # WHEN: recursively_delete_group() is called + self.media_item.recursively_delete_group(test_group) + + # THEN: + assert mocked_delete_file.call_count == 0, u'delete_file() should not be called' + assert self.media_item.manager.delete_object.call_count == 7, \ + u'manager.delete_object() should be called exactly 7 times' + + # CLEANUP: Remove added attribute from ImageFilenames and ImageGroups + delattr(ImageFilenames, 'group_id') + delattr(ImageGroups, 'parent_id') 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: Fri, 29 Mar 2013 20:58:06 +0000 Subject: [PATCH 094/235] 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 095/235] 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 096/235] 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:33:21 +0100 Subject: [PATCH 097/235] use QActionGroup instead of checkboxes Fixes: https://launchpad.net/bugs/1103906 --- openlp/core/ui/servicemanager.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 0ac711492..c00eb118d 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -234,18 +234,22 @@ class ServiceManagerDialog(object): icon=u':/general/general_edit.png', triggers=self.create_custom) self.menu.addSeparator() # Add AutoPlay menu actions - self.auto_play_slides_group = QtGui.QMenu(translate('OpenLP.ServiceManager', '&Auto play slides')) - self.menu.addMenu(self.auto_play_slides_group) - self.auto_play_slides_loop = create_widget_action(self.auto_play_slides_group, + self.auto_play_slides_menu = QtGui.QMenu(translate('OpenLP.ServiceManager', '&Auto play slides')) + self.menu.addMenu(self.auto_play_slides_menu) + auto_play_slides_group = QtGui.QActionGroup(self.auto_play_slides_menu) + auto_play_slides_group.setExclusive(True) + self.auto_play_slides_loop = create_widget_action(self.auto_play_slides_menu, text=translate('OpenLP.ServiceManager', 'Auto play slides &Loop'), checked=False, triggers=self.toggle_auto_play_slides_loop) - self.auto_play_slides_once = create_widget_action(self.auto_play_slides_group, + auto_play_slides_group.addAction(self.auto_play_slides_loop) + self.auto_play_slides_once = create_widget_action(self.auto_play_slides_menu, text=translate('OpenLP.ServiceManager', 'Auto play slides &Once'), checked=False, triggers=self.toggle_auto_play_slides_once) - self.auto_play_slides_group.addSeparator() - self.timed_slide_interval = create_widget_action(self.auto_play_slides_group, + auto_play_slides_group.addAction(self.auto_play_slides_once) + self.auto_play_slides_menu.addSeparator() + self.timed_slide_interval = create_widget_action(self.auto_play_slides_menu, text=translate('OpenLP.ServiceManager', '&Delay between slides'), - checked=False, triggers=self.on_timed_slide_interval) + triggers=self.on_timed_slide_interval) self.menu.addSeparator() self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'), icon=u':/general/general_preview.png', triggers=self.make_preview) @@ -786,7 +790,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.notes_action.setVisible(True) if service_item[u'service_item'].is_capable(ItemCapabilities.CanLoop) and \ len(service_item[u'service_item'].get_frames()) > 1: - self.auto_play_slides_group.menuAction().setVisible(True) + self.auto_play_slides_menu.menuAction().setVisible(True) self.auto_play_slides_once.setChecked(service_item[u'service_item'].auto_play_slides_once) self.auto_play_slides_loop.setChecked(service_item[u'service_item'].auto_play_slides_loop) self.timed_slide_interval.setChecked(service_item[u'service_item'].timed_slide_interval > 0) @@ -798,7 +802,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): translate('OpenLP.ServiceManager', '&Delay between slides') + delay_suffix) # TODO for future: make group explains itself more visually else: - self.auto_play_slides_group.menuAction().setVisible(False) + self.auto_play_slides_menu.menuAction().setVisible(False) if service_item[u'service_item'].is_capable(ItemCapabilities.HasVariableStartTime): self.time_action.setVisible(True) if service_item[u'service_item'].is_capable(ItemCapabilities.CanAutoStartForLive): From 85f64ddae66ca7ea5a62f23ad791a37e459d01be Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 30 Mar 2013 10:46:32 +0100 Subject: [PATCH 098/235] 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 099/235] 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 906c090e8d8adba67256afd5e3e14bdef8813f77 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 30 Mar 2013 15:45:02 +0100 Subject: [PATCH 100/235] added test --- .../openlp_core_ui/test_servicemanager.py | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/test_servicemanager.py b/tests/interfaces/openlp_core_ui/test_servicemanager.py index 4e309e889..0b631c61a 100644 --- a/tests/interfaces/openlp_core_ui/test_servicemanager.py +++ b/tests/interfaces/openlp_core_ui/test_servicemanager.py @@ -3,11 +3,11 @@ """ from unittest import TestCase -from mock import MagicMock, patch +from mock import MagicMock, Mock, patch from PyQt4 import QtGui -from openlp.core.lib import Registry, ScreenList +from openlp.core.lib import Registry, ScreenList, ServiceItem from openlp.core.ui.mainwindow import MainWindow @@ -42,3 +42,44 @@ class TestServiceManager(TestCase): # THEN the count of items should be zero self.assertEqual(self.service_manager.service_manager_list.topLevelItemCount(), 0, u'The service manager list should be empty ') + + def context_menu_test(self): + """ + Test the context_menu() method. + """ + # GIVEN: A service item added + with patch(u'PyQt4.QtGui.QTreeWidget.itemAt') as mocked_item_at_method, \ + patch(u'PyQt4.QtGui.QWidget.mapToGlobal') as mocked_map_to_global, \ + patch(u'PyQt4.QtGui.QMenu.exec_') as mocked_exec: + mocked_item = MagicMock() + mocked_item.parent.return_value = None + mocked_item_at_method.return_value = mocked_item + # We want 1 to be returned for the position + mocked_item.data.return_value = 1 + # A service item without capabilities. + service_item = ServiceItem() + self.service_manager.service_items = [{u'service_item': service_item}] + q_point = None + # Mocked actions. + self.service_manager.edit_action.setVisible = Mock() + self.service_manager.create_custom_action.setVisible = Mock() + self.service_manager.maintain_action.setVisible = Mock() + self.service_manager.notes_action.setVisible = Mock() + self.service_manager.time_action.setVisible = Mock() + self.service_manager.auto_start_action.setVisible = Mock() + + # WHEN: Show the context menu. + self.service_manager.context_menu(q_point) + + # THEN: The following actions should be not visible. + self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ + u'The action should be set invisible.' + self.service_manager.create_custom_action.setVisible.assert_called_with(False), \ + u'The action should be set invisible.' + self.service_manager.maintain_action.setVisible.assert_called_with(False), \ + u'The action should be set invisible.' + self.service_manager.notes_action.setVisible.assert_called_with(True), u'The action should be set visible.' + self.service_manager.time_action.setVisible.assert_called_with(False), \ + u'The action should be set invisible.' + self.service_manager.auto_start_action.setVisible.assert_called_with(False), \ + u'The action should be set invisible.' From a9cb8b07a7f9dedaed3f098240be72411b9ee59e Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 30 Mar 2013 15:47:38 +0100 Subject: [PATCH 101/235] fixed assertion test --- tests/interfaces/openlp_core_ui/test_servicemanager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/interfaces/openlp_core_ui/test_servicemanager.py b/tests/interfaces/openlp_core_ui/test_servicemanager.py index 0b631c61a..a651ca29c 100644 --- a/tests/interfaces/openlp_core_ui/test_servicemanager.py +++ b/tests/interfaces/openlp_core_ui/test_servicemanager.py @@ -74,12 +74,12 @@ class TestServiceManager(TestCase): # THEN: The following actions should be not visible. self.service_manager.edit_action.setVisible.assert_called_once_with(False), \ u'The action should be set invisible.' - self.service_manager.create_custom_action.setVisible.assert_called_with(False), \ + self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \ u'The action should be set invisible.' - self.service_manager.maintain_action.setVisible.assert_called_with(False), \ + self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \ u'The action should be set invisible.' self.service_manager.notes_action.setVisible.assert_called_with(True), u'The action should be set visible.' - self.service_manager.time_action.setVisible.assert_called_with(False), \ + self.service_manager.time_action.setVisible.assert_called_once_with(False), \ u'The action should be set invisible.' - self.service_manager.auto_start_action.setVisible.assert_called_with(False), \ + self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \ u'The action should be set invisible.' From 76ea8126294d088d5ae4e002cd860b3f1faf7764 Mon Sep 17 00:00:00 2001 From: M2j Date: Sat, 30 Mar 2013 22:54:42 +0100 Subject: [PATCH 102/235] - 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 103/235] 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 104/235] 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 105/235] 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 106/235] 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 107/235] - 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 108/235] 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 109/235] 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 6c4b282d208eead548c6e955056838202b06ec7e Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 09:32:58 +0200 Subject: [PATCH 110/235] updated vly.pc --- openlp/core/ui/media/vendor/vlc.py | 227 +++++++++++++++++------------ 1 file changed, 130 insertions(+), 97 deletions(-) diff --git a/openlp/core/ui/media/vendor/vlc.py b/openlp/core/ui/media/vendor/vlc.py index dbb2971f7..f2cb3cad4 100644 --- a/openlp/core/ui/media/vendor/vlc.py +++ b/openlp/core/ui/media/vendor/vlc.py @@ -48,7 +48,7 @@ import sys from inspect import getargspec __version__ = "N/A" -build_date = "Thu Mar 21 22:33:03 2013" +build_date = "Mon Apr 1 23:47:38 2013" if sys.version_info[0] > 2: str = str @@ -70,7 +70,7 @@ if sys.version_info[0] > 2: if isinstance(b, bytes): return b.decode(sys.getfilesystemencoding()) else: - return str(b) + return b else: str = str unicode = unicode @@ -278,6 +278,11 @@ def class_result(classname): return classname(result) return wrap_errcheck +# Wrapper for the opaque struct libvlc_log_t +class Log(ctypes.Structure): + pass +Log_ptr = ctypes.POINTER(Log) + # FILE* ctypes wrapper, copied from # http://svn.python.org/projects/ctypes/trunk/ctypeslib/ctypeslib/contrib/pythonhdr.py class FILE(ctypes.Structure): @@ -675,11 +680,14 @@ class Callback(ctypes.c_void_p): pass class LogCb(ctypes.c_void_p): """Callback prototype for LibVLC log message handler. -\param data data pointer as given to L{libvlc_log_subscribe}() +\param data data pointer as given to L{libvlc_log_set}() \param level message level (@ref enum libvlc_log_level) +\param ctx message context (meta-informations about the message) \param fmt printf() format string (as defined by ISO C11) \param args variable argument list for the format \note Log message handlers must be thread-safe. +\warning The message context pointer, the format string parameters and the + variable arguments are only valid until the callback returns. """ pass class VideoLockCb(ctypes.c_void_p): @@ -813,13 +821,16 @@ class CallbackDecorators(object): Callback.__doc__ = '''Callback function notification \param p_event the event triggering the callback ''' - LogCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p) + LogCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, Log_ptr, ctypes.c_char_p, ctypes.c_void_p) LogCb.__doc__ = '''Callback prototype for LibVLC log message handler. -\param data data pointer as given to L{libvlc_log_subscribe}() +\param data data pointer as given to L{libvlc_log_set}() \param level message level (@ref enum libvlc_log_level) +\param ctx message context (meta-informations about the message) \param fmt printf() format string (as defined by ISO C11) \param args variable argument list for the format \note Log message handlers must be thread-safe. +\warning The message context pointer, the format string parameters and the + variable arguments are only valid until the callback returns. ''' VideoLockCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ListPOINTER(ctypes.c_void_p)) VideoLockCb.__doc__ = '''Callback prototype to allocate and lock a picture buffer. @@ -1407,7 +1418,7 @@ class Instance(_Ctype): @param name: interface name, or NULL for default. @return: 0 on success, -1 on error. ''' - return libvlc_add_intf(self, name) + return libvlc_add_intf(self, str_to_bytes(name)) def set_user_agent(self, name, http): '''Sets the application name. LibVLC passes this as the user agent string @@ -1416,7 +1427,33 @@ class Instance(_Ctype): @param http: HTTP User Agent, e.g. "FooBar/1.2.3 Python/2.6.0". @version: LibVLC 1.1.1 or later. ''' - return libvlc_set_user_agent(self, name, http) + return libvlc_set_user_agent(self, str_to_bytes(name), str_to_bytes(http)) + + def log_unset(self): + '''Unsets the logging callback for a LibVLC instance. This is rarely needed: + the callback is implicitly unset when the instance is destroyed. + This function will wait for any pending callbacks invocation to complete + (causing a deadlock if called from within the callback). + @version: LibVLC 2.1.0 or later. + ''' + return libvlc_log_unset(self) + + def log_set(self, data, p_instance): + '''Sets the logging callback for a LibVLC instance. + This function is thread-safe: it will wait for any pending callbacks + invocation to complete. + @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while is being initialized. These messages cannot be captured with this interface. @warning A deadlock may occur if this function is called from the callback. + @param p_instance: libvlc instance. + @version: LibVLC 2.1.0 or later. + ''' + return libvlc_log_set(self, data, p_instance) + + def log_set_file(self, stream): + '''Sets up logging to a file. + @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{log_unset}()). + @version: LibVLC 2.1.0 or later. + ''' + return libvlc_log_set_file(self, stream) def media_new_location(self, psz_mrl): '''Create a media with a certain given media resource location, @@ -1429,7 +1466,7 @@ class Instance(_Ctype): @param psz_mrl: the media location. @return: the newly created media or NULL on error. ''' - return libvlc_media_new_location(self, psz_mrl) + return libvlc_media_new_location(self, str_to_bytes(psz_mrl)) def media_new_path(self, path): '''Create a media for a certain file path. @@ -1437,7 +1474,7 @@ class Instance(_Ctype): @param path: local filesystem path. @return: the newly created media or NULL on error. ''' - return libvlc_media_new_path(self, path) + return libvlc_media_new_path(self, str_to_bytes(path)) def media_new_fd(self, fd): '''Create a media for an already open file descriptor. @@ -1465,14 +1502,14 @@ class Instance(_Ctype): @param psz_name: the name of the node. @return: the new empty media or NULL on error. ''' - return libvlc_media_new_as_node(self, psz_name) + return libvlc_media_new_as_node(self, str_to_bytes(psz_name)) def media_discoverer_new_from_name(self, psz_name): '''Discover media service by name. @param psz_name: service name. @return: media discover object or NULL in case of error. ''' - return libvlc_media_discoverer_new_from_name(self, psz_name) + return libvlc_media_discoverer_new_from_name(self, str_to_bytes(psz_name)) def media_library_new(self): '''Create an new Media Library object. @@ -1500,7 +1537,7 @@ class Instance(_Ctype): @return: A NULL-terminated linked list of potential audio output devices. It must be freed it with L{audio_output_device_list_release}(). @version: LibVLC 2.1.0 or later. ''' - return libvlc_audio_output_device_list_get(self, aout) + return libvlc_audio_output_device_list_get(self, str_to_bytes(aout)) def vlm_release(self): '''Release the vlm instance related to the given L{Instance}. @@ -1518,7 +1555,7 @@ class Instance(_Ctype): @param b_loop: Should this broadcast be played in loop ? @return: 0 on success, -1 on error. ''' - return libvlc_vlm_add_broadcast(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop) + return libvlc_vlm_add_broadcast(self, str_to_bytes(psz_name), str_to_bytes(psz_input), str_to_bytes(psz_output), i_options, ppsz_options, b_enabled, b_loop) def vlm_add_vod(self, psz_name, psz_input, i_options, ppsz_options, b_enabled, psz_mux): '''Add a vod, with one input. @@ -1530,14 +1567,14 @@ class Instance(_Ctype): @param psz_mux: the muxer of the vod media. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_add_vod(self, psz_name, psz_input, i_options, ppsz_options, b_enabled, psz_mux) + return libvlc_vlm_add_vod(self, str_to_bytes(psz_name), str_to_bytes(psz_input), i_options, ppsz_options, b_enabled, str_to_bytes(psz_mux)) def vlm_del_media(self, psz_name): '''Delete a media (VOD or broadcast). @param psz_name: the media to delete. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_del_media(self, psz_name) + return libvlc_vlm_del_media(self, str_to_bytes(psz_name)) def vlm_set_enabled(self, psz_name, b_enabled): '''Enable or disable a media (VOD or broadcast). @@ -1545,7 +1582,7 @@ class Instance(_Ctype): @param b_enabled: the new status. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_set_enabled(self, psz_name, b_enabled) + return libvlc_vlm_set_enabled(self, str_to_bytes(psz_name), b_enabled) def vlm_set_output(self, psz_name, psz_output): '''Set the output for a media. @@ -1553,7 +1590,7 @@ class Instance(_Ctype): @param psz_output: the output MRL (the parameter to the "sout" variable). @return: 0 on success, -1 on error. ''' - return libvlc_vlm_set_output(self, psz_name, psz_output) + return libvlc_vlm_set_output(self, str_to_bytes(psz_name), str_to_bytes(psz_output)) def vlm_set_input(self, psz_name, psz_input): '''Set a media's input MRL. This will delete all existing inputs and @@ -1562,7 +1599,7 @@ class Instance(_Ctype): @param psz_input: the input MRL. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_set_input(self, psz_name, psz_input) + return libvlc_vlm_set_input(self, str_to_bytes(psz_name), str_to_bytes(psz_input)) def vlm_add_input(self, psz_name, psz_input): '''Add a media's input MRL. This will add the specified one. @@ -1570,7 +1607,7 @@ class Instance(_Ctype): @param psz_input: the input MRL. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_add_input(self, psz_name, psz_input) + return libvlc_vlm_add_input(self, str_to_bytes(psz_name), str_to_bytes(psz_input)) def vlm_set_loop(self, psz_name, b_loop): '''Set a media's loop status. @@ -1578,7 +1615,7 @@ class Instance(_Ctype): @param b_loop: the new status. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_set_loop(self, psz_name, b_loop) + return libvlc_vlm_set_loop(self, str_to_bytes(psz_name), b_loop) def vlm_set_mux(self, psz_name, psz_mux): '''Set a media's vod muxer. @@ -1586,7 +1623,7 @@ class Instance(_Ctype): @param psz_mux: the new muxer. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_set_mux(self, psz_name, psz_mux) + return libvlc_vlm_set_mux(self, str_to_bytes(psz_name), str_to_bytes(psz_mux)) def vlm_change_media(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop): '''Edit the parameters of a media. This will delete all existing inputs and @@ -1600,28 +1637,28 @@ class Instance(_Ctype): @param b_loop: Should this broadcast be played in loop ? @return: 0 on success, -1 on error. ''' - return libvlc_vlm_change_media(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop) + return libvlc_vlm_change_media(self, str_to_bytes(psz_name), str_to_bytes(psz_input), str_to_bytes(psz_output), i_options, ppsz_options, b_enabled, b_loop) def vlm_play_media(self, psz_name): '''Play the named broadcast. @param psz_name: the name of the broadcast. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_play_media(self, psz_name) + return libvlc_vlm_play_media(self, str_to_bytes(psz_name)) def vlm_stop_media(self, psz_name): '''Stop the named broadcast. @param psz_name: the name of the broadcast. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_stop_media(self, psz_name) + return libvlc_vlm_stop_media(self, str_to_bytes(psz_name)) def vlm_pause_media(self, psz_name): '''Pause the named broadcast. @param psz_name: the name of the broadcast. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_pause_media(self, psz_name) + return libvlc_vlm_pause_media(self, str_to_bytes(psz_name)) def vlm_seek_media(self, psz_name, f_percentage): '''Seek in the named broadcast. @@ -1629,7 +1666,7 @@ class Instance(_Ctype): @param f_percentage: the percentage to seek to. @return: 0 on success, -1 on error. ''' - return libvlc_vlm_seek_media(self, psz_name, f_percentage) + return libvlc_vlm_seek_media(self, str_to_bytes(psz_name), f_percentage) def vlm_show_media(self, psz_name): '''Return information about the named media as a JSON @@ -1643,7 +1680,7 @@ class Instance(_Ctype): @param psz_name: the name of the media, if the name is an empty string, all media is described. @return: string with information about named media, or NULL on error. ''' - return libvlc_vlm_show_media(self, psz_name) + return libvlc_vlm_show_media(self, str_to_bytes(psz_name)) def vlm_get_media_instance_position(self, psz_name, i_instance): '''Get vlm_media instance position by name or instance id. @@ -1651,7 +1688,7 @@ class Instance(_Ctype): @param i_instance: instance id. @return: position as float or -1. on error. ''' - return libvlc_vlm_get_media_instance_position(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_position(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_time(self, psz_name, i_instance): '''Get vlm_media instance time by name or instance id. @@ -1659,7 +1696,7 @@ class Instance(_Ctype): @param i_instance: instance id. @return: time as integer or -1 on error. ''' - return libvlc_vlm_get_media_instance_time(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_time(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_length(self, psz_name, i_instance): '''Get vlm_media instance length by name or instance id. @@ -1667,7 +1704,7 @@ class Instance(_Ctype): @param i_instance: instance id. @return: length of media item or -1 on error. ''' - return libvlc_vlm_get_media_instance_length(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_length(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_rate(self, psz_name, i_instance): '''Get vlm_media instance playback rate by name or instance id. @@ -1675,7 +1712,7 @@ class Instance(_Ctype): @param i_instance: instance id. @return: playback rate or -1 on error. ''' - return libvlc_vlm_get_media_instance_rate(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_rate(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_title(self, psz_name, i_instance): '''Get vlm_media instance title number by name or instance id. @@ -1684,7 +1721,7 @@ class Instance(_Ctype): @return: title as number or -1 on error. @bug: will always return 0. ''' - return libvlc_vlm_get_media_instance_title(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_title(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_chapter(self, psz_name, i_instance): '''Get vlm_media instance chapter number by name or instance id. @@ -1693,7 +1730,7 @@ class Instance(_Ctype): @return: chapter as number or -1 on error. @bug: will always return 0. ''' - return libvlc_vlm_get_media_instance_chapter(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_chapter(self, str_to_bytes(psz_name), i_instance) def vlm_get_media_instance_seekable(self, psz_name, i_instance): '''Is libvlc instance seekable ? @@ -1702,7 +1739,7 @@ class Instance(_Ctype): @return: 1 if seekable, 0 if not, -1 if media does not exist. @bug: will always return 0. ''' - return libvlc_vlm_get_media_instance_seekable(self, psz_name, i_instance) + return libvlc_vlm_get_media_instance_seekable(self, str_to_bytes(psz_name), i_instance) def vlm_get_event_manager(self): '''Get libvlc_event_manager from a vlm media. @@ -1751,7 +1788,7 @@ class Media(_Ctype): self.add_option(o) - def add_option(self, ppsz_options): + def add_option(self, psz_options): '''Add an option to the media. This option will be used to determine how the media_player will read the media. This allows to use VLC's advanced @@ -1763,11 +1800,11 @@ class Media(_Ctype): Specifically, due to architectural issues most audio and video options, such as text renderer options, have no effects on an individual media. These options must be set through L{new}() instead. - @param ppsz_options: the options (as a string). + @param psz_options: the options (as a string). ''' - return libvlc_media_add_option(self, ppsz_options) + return libvlc_media_add_option(self, str_to_bytes(psz_options)) - def add_option_flag(self, ppsz_options, i_flags): + def add_option_flag(self, psz_options, i_flags): '''Add an option to the media with configurable flags. This option will be used to determine how the media_player will read the media. This allows to use VLC's advanced @@ -1777,10 +1814,10 @@ class Media(_Ctype): specifically, due to architectural issues, video-related options such as text renderer options cannot be set on a single media. They must be set on the whole libvlc instance instead. - @param ppsz_options: the options (as a string). + @param psz_options: the options (as a string). @param i_flags: the flags for this option. ''' - return libvlc_media_add_option_flag(self, ppsz_options, i_flags) + return libvlc_media_add_option_flag(self, str_to_bytes(psz_options), i_flags) def retain(self): '''Retain a reference to a media descriptor object (libvlc_media_t). Use @@ -1829,7 +1866,7 @@ class Media(_Ctype): @param e_meta: the meta to write. @param psz_value: the media's meta. ''' - return libvlc_media_set_meta(self, e_meta, psz_value) + return libvlc_media_set_meta(self, e_meta, str_to_bytes(psz_value)) def save_meta(self): '''Save the meta previously set. @@ -2490,7 +2527,7 @@ class MediaPlayer(_Ctype): @version: LibVLC 1.1.1 or later. @bug: All pixel planes are expected to have the same pitch. To use the YCbCr color space with chrominance subsampling, consider using L{video_set_format_callbacks}() instead. ''' - return libvlc_video_set_format(self, chroma, width, height, pitch) + return libvlc_video_set_format(self, str_to_bytes(chroma), width, height, pitch) def video_set_format_callbacks(self, setup, cleanup): '''Set decoded video chroma and dimensions. This only works in combination with @@ -2617,7 +2654,7 @@ class MediaPlayer(_Ctype): @param channels: channels count. @version: LibVLC 2.0.0 or later. ''' - return libvlc_audio_set_format(self, format, rate, channels) + return libvlc_audio_set_format(self, str_to_bytes(format), rate, channels) def get_length(self): '''Get the current movie length (in ms). @@ -2843,7 +2880,7 @@ class MediaPlayer(_Ctype): '''Set new video aspect ratio. @param psz_aspect: new video aspect-ratio or NULL to reset to default @note Invalid aspect ratios are ignored. ''' - return libvlc_video_set_aspect_ratio(self, psz_aspect) + return libvlc_video_set_aspect_ratio(self, str_to_bytes(psz_aspect)) def video_get_spu(self): '''Get current video subtitle. @@ -2859,7 +2896,7 @@ class MediaPlayer(_Ctype): def video_set_spu(self, i_spu): '''Set new video subtitle. - @param i_spu: new video subtitle to select. + @param i_spu: video subtitle track to select (i_id from track description). @return: 0 on success, -1 if out of range. ''' return libvlc_video_set_spu(self, i_spu) @@ -2869,7 +2906,7 @@ class MediaPlayer(_Ctype): @param psz_subtitle: new video subtitle file. @return: the success status (boolean). ''' - return libvlc_video_set_subtitle_file(self, psz_subtitle) + return libvlc_video_set_subtitle_file(self, str_to_bytes(psz_subtitle)) def video_get_spu_delay(self): '''Get the current subtitle delay. Positive values means subtitles are being @@ -2900,7 +2937,7 @@ class MediaPlayer(_Ctype): '''Set new crop filter geometry. @param psz_geometry: new crop filter geometry (NULL to unset). ''' - return libvlc_video_set_crop_geometry(self, psz_geometry) + return libvlc_video_set_crop_geometry(self, str_to_bytes(psz_geometry)) def video_get_teletext(self): '''Get current teletext page requested. @@ -2948,13 +2985,13 @@ class MediaPlayer(_Ctype): @param i_height: the snapshot's height. @return: 0 on success, -1 if the video was not found. ''' - return libvlc_video_take_snapshot(self, num, psz_filepath, i_width, i_height) + return libvlc_video_take_snapshot(self, num, str_to_bytes(psz_filepath), i_width, i_height) def video_set_deinterlace(self, psz_mode): '''Enable or disable deinterlace filter. @param psz_mode: type of deinterlace filter, NULL to disable. ''' - return libvlc_video_set_deinterlace(self, psz_mode) + return libvlc_video_set_deinterlace(self, str_to_bytes(psz_mode)) def video_get_marquee_int(self, option): '''Get an integer marquee option value. @@ -2982,7 +3019,7 @@ class MediaPlayer(_Ctype): @param option: marq option to set See libvlc_video_marquee_string_option_t. @param psz_text: marq option value. ''' - return libvlc_video_set_marquee_string(self, option, psz_text) + return libvlc_video_set_marquee_string(self, option, str_to_bytes(psz_text)) def video_get_logo_int(self, option): '''Get integer logo option. @@ -3006,7 +3043,7 @@ class MediaPlayer(_Ctype): @param option: logo option to set, values of libvlc_video_logo_option_t. @param psz_value: logo option value. ''' - return libvlc_video_set_logo_string(self, option, psz_value) + return libvlc_video_set_logo_string(self, option, str_to_bytes(psz_value)) def video_get_adjust_int(self, option): '''Get integer adjust option. @@ -3049,7 +3086,7 @@ class MediaPlayer(_Ctype): @param psz_name: name of audio output, use psz_name of See L{AudioOutput}. @return: 0 if function succeded, -1 on error. ''' - return libvlc_audio_output_set(self, psz_name) + return libvlc_audio_output_set(self, str_to_bytes(psz_name)) def audio_output_device_set(self, psz_audio_output, psz_device_id): '''Configures an explicit audio output device for a given audio output plugin. @@ -3065,7 +3102,7 @@ class MediaPlayer(_Ctype): @param psz_device_id: device. @return: Nothing. Errors are ignored. ''' - return libvlc_audio_output_device_set(self, psz_audio_output, psz_device_id) + return libvlc_audio_output_device_set(self, str_to_bytes(psz_audio_output), str_to_bytes(psz_device_id)) def audio_toggle_mute(self): '''Toggle mute status. @@ -3314,44 +3351,43 @@ def libvlc_event_type_name(event_type): ctypes.c_char_p, ctypes.c_uint) return f(event_type) -def libvlc_log_subscribe(sub, cb, data): - '''Registers a logging callback to LibVLC. - This function is thread-safe. - @param sub: uninitialized subscriber structure. +def libvlc_log_unset(p_instance): + '''Unsets the logging callback for a LibVLC instance. This is rarely needed: + the callback is implicitly unset when the instance is destroyed. + This function will wait for any pending callbacks invocation to complete + (causing a deadlock if called from within the callback). + @param p_instance: libvlc instance. + @version: LibVLC 2.1.0 or later. + ''' + f = _Cfunctions.get('libvlc_log_unset', None) or \ + _Cfunction('libvlc_log_unset', ((1,),), None, + None, Instance) + return f(p_instance) + +def libvlc_log_set(cb, data, p_instance): + '''Sets the logging callback for a LibVLC instance. + This function is thread-safe: it will wait for any pending callbacks + invocation to complete. @param cb: callback function pointer. - @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while initializing, before any LibVLC instance even exists. Thus this function does not require a LibVLC instance parameter. @warning As a consequence of not depending on a LibVLC instance, all logging callbacks are shared by all LibVLC instances within the process / address space. This also enables log messages to be emitted by LibVLC components that are not specific to any given LibVLC instance. @warning Do not call this function from within a logging callback. It would trigger a dead lock. + @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while is being initialized. These messages cannot be captured with this interface. @warning A deadlock may occur if this function is called from the callback. + @param p_instance: libvlc instance. @version: LibVLC 2.1.0 or later. ''' - f = _Cfunctions.get('libvlc_log_subscribe', None) or \ - _Cfunction('libvlc_log_subscribe', ((1,), (1,), (1,),), None, - None, ctypes.c_void_p, LogCb, ctypes.c_void_p) - return f(sub, cb, data) + f = _Cfunctions.get('libvlc_log_set', None) or \ + _Cfunction('libvlc_log_set', ((1,), (1,), (1,),), None, + None, Instance, LogCb, ctypes.c_void_p) + return f(cb, data, p_instance) -def libvlc_log_subscribe_file(sub, stream): - '''Registers a logging callback to a file. - @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{libvlc_log_unsubscribe}()). +def libvlc_log_set_file(p_instance, stream): + '''Sets up logging to a file. + @param p_instance: libvlc instance. + @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{libvlc_log_unset}()). @version: LibVLC 2.1.0 or later. ''' - f = _Cfunctions.get('libvlc_log_subscribe_file', None) or \ - _Cfunction('libvlc_log_subscribe_file', ((1,), (1,),), None, - None, ctypes.c_void_p, FILE_ptr) - return f(sub, stream) - -def libvlc_log_unsubscribe(sub): - '''Deregisters a logging callback from LibVLC. - This function is thread-safe. - @note: After (and only after) L{libvlc_log_unsubscribe}() has returned, - LibVLC warrants that there are no more pending calls of the subscription - callback function. - @warning: Do not call this function from within a logging callback. - It would trigger a dead lock. - @param sub: initialized subscriber structure. - @version: LibVLC 2.1.0 or later. - ''' - f = _Cfunctions.get('libvlc_log_unsubscribe', None) or \ - _Cfunction('libvlc_log_unsubscribe', ((1,),), None, - None, ctypes.c_void_p) - return f(sub) + f = _Cfunctions.get('libvlc_log_set_file', None) or \ + _Cfunction('libvlc_log_set_file', ((1,), (1,),), None, + None, Instance, FILE_ptr) + return f(p_instance, stream) def libvlc_module_description_list_release(p_list): '''Release a list of module descriptions. @@ -3460,7 +3496,7 @@ def libvlc_media_new_as_node(p_instance, psz_name): ctypes.c_void_p, Instance, ctypes.c_char_p) return f(p_instance, psz_name) -def libvlc_media_add_option(p_md, ppsz_options): +def libvlc_media_add_option(p_md, psz_options): '''Add an option to the media. This option will be used to determine how the media_player will read the media. This allows to use VLC's advanced @@ -3473,14 +3509,14 @@ def libvlc_media_add_option(p_md, ppsz_options): such as text renderer options, have no effects on an individual media. These options must be set through L{libvlc_new}() instead. @param p_md: the media descriptor. - @param ppsz_options: the options (as a string). + @param psz_options: the options (as a string). ''' f = _Cfunctions.get('libvlc_media_add_option', None) or \ _Cfunction('libvlc_media_add_option', ((1,), (1,),), None, None, Media, ctypes.c_char_p) - return f(p_md, ppsz_options) + return f(p_md, psz_options) -def libvlc_media_add_option_flag(p_md, ppsz_options, i_flags): +def libvlc_media_add_option_flag(p_md, psz_options, i_flags): '''Add an option to the media with configurable flags. This option will be used to determine how the media_player will read the media. This allows to use VLC's advanced @@ -3491,13 +3527,13 @@ def libvlc_media_add_option_flag(p_md, ppsz_options, i_flags): such as text renderer options cannot be set on a single media. They must be set on the whole libvlc instance instead. @param p_md: the media descriptor. - @param ppsz_options: the options (as a string). + @param psz_options: the options (as a string). @param i_flags: the flags for this option. ''' f = _Cfunctions.get('libvlc_media_add_option_flag', None) or \ _Cfunction('libvlc_media_add_option_flag', ((1,), (1,), (1,),), None, None, Media, ctypes.c_char_p, ctypes.c_uint) - return f(p_md, ppsz_options, i_flags) + return f(p_md, psz_options, i_flags) def libvlc_media_retain(p_md): '''Retain a reference to a media descriptor object (libvlc_media_t). Use @@ -4949,12 +4985,12 @@ def libvlc_video_get_spu_description(p_mi): def libvlc_video_set_spu(p_mi, i_spu): '''Set new video subtitle. @param p_mi: the media player. - @param i_spu: new video subtitle to select. + @param i_spu: video subtitle track to select (i_id from track description). @return: 0 on success, -1 if out of range. ''' f = _Cfunctions.get('libvlc_video_set_spu', None) or \ _Cfunction('libvlc_video_set_spu', ((1,), (1,),), None, - ctypes.c_int, MediaPlayer, ctypes.c_uint) + ctypes.c_int, MediaPlayer, ctypes.c_int) return f(p_mi, i_spu) def libvlc_video_set_subtitle_file(p_mi, psz_subtitle): @@ -5791,7 +5827,7 @@ def libvlc_vlm_get_event_manager(p_instance): # libvlc_printerr # libvlc_set_exit_handler -# 18 function(s) not wrapped as methods: +# 15 function(s) not wrapped as methods: # libvlc_audio_output_device_list_release # libvlc_audio_output_list_release # libvlc_clearerr @@ -5802,9 +5838,6 @@ def libvlc_vlm_get_event_manager(p_instance): # libvlc_get_changeset # libvlc_get_compiler # libvlc_get_version -# libvlc_log_subscribe -# libvlc_log_subscribe_file -# libvlc_log_unsubscribe # libvlc_media_tracks_release # libvlc_module_description_list_release # libvlc_new From e3afbefd27de25b73ef411e5b8ec2471d0f55116 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 2 Apr 2013 16:42:33 +0200 Subject: [PATCH 111/235] 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 112/235] 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 113/235] 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 114/235] 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 115/235] 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 116/235] 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 117/235] 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 118/235] 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 119/235] 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 120/235] 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 121/235] 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 122/235] 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 123/235] 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 124/235] 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 125/235] 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 69009970c07641217ea2c1a8eee8810152b01812 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 5 Apr 2013 21:58:13 +0200 Subject: [PATCH 126/235] replaced BeautifulSoup3 by BeautifulSoup4 --- openlp/core/ui/exceptionform.py | 2 +- openlp/plugins/bibles/lib/http.py | 106 ++++++++++++++---------------- scripts/check_dependencies.py | 2 +- 3 files changed, 52 insertions(+), 58 deletions(-) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 25f8201b1..f7dc83d29 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -35,7 +35,7 @@ import os import platform import sqlalchemy -import BeautifulSoup +from bs4 import BeautifulSoup from lxml import etree from PyQt4 import Qt, QtCore, QtGui, QtWebKit diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index b01377a05..390ac1cd9 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -36,7 +36,7 @@ import socket import urllib from HTMLParser import HTMLParseError -from BeautifulSoup import BeautifulSoup, NavigableString, Tag +from bs4 import BeautifulSoup, NavigableString, Tag from openlp.core.lib import Registry, translate from openlp.core.lib.ui import critical_error_message_box @@ -44,6 +44,9 @@ from openlp.core.utils import get_web_page from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book +CLEANER_REGEX = re.compile(' |
|\'\+\'') +FIX_PUNKCTUATION_REGEX = re.compile(r'[ ]+([.,;])') +REDUCE_SPACES_REGEX = re.compile(r'[ ]{2,}') UGLY_CHARS = { u'\u2014': u' - ', u'\u2018': u'\'', @@ -52,9 +55,12 @@ UGLY_CHARS = { u'\u201d': u'"', u' ': u' ' } +VERSE_NUMBER_REGEX = re.compile(r'v(\d{1,2})(\d{3})(\d{3}) verse.*') + log = logging.getLogger(__name__) + class BGExtract(object): """ Extract verses from BibleGateway @@ -78,9 +84,9 @@ class BGExtract(object): An HTML class attribute for further qualification. """ if class_: - all_tags = parent.findAll(tag, class_) + all_tags = parent.find_all(tag, class_) else: - all_tags = parent.findAll(tag) + all_tags = parent.find_all(tag) for element in all_tags: element.extract() @@ -173,8 +179,8 @@ class BGExtract(object): def _extract_verses_old(self, div): """ - Use the old style of parsing for those Bibles on BG who mysteriously - have not been migrated to the new (still broken) HTML. + Use the old style of parsing for those Bibles on BG who mysteriously have not been migrated to the new (still + broken) HTML. ``div`` The parent div. @@ -185,13 +191,12 @@ class BGExtract(object): if first_verse and first_verse.contents: verse_list[1] = unicode(first_verse.contents[0]) for verse in div(u'sup', u'versenum'): - raw_verse_num = verse.next + raw_verse_num = verse.next_element clean_verse_num = 0 - # Not all verses exist in all translations and may or may not be - # represented by a verse number. If they are not fine, if they are - # it will probably be in a format that breaks int(). We will then - # have no idea what garbage may be sucked in to the verse text so - # if we do not get a clean int() then ignore the verse completely. + # Not all verses exist in all translations and may or may not be represented by a verse number. If they are + # not fine, if they are it will probably be in a format that breaks int(). We will then have no idea what + # garbage may be sucked in to the verse text so if we do not get a clean int() then ignore the verse + # completely. try: clean_verse_num = int(str(raw_verse_num)) except ValueError: @@ -201,16 +206,16 @@ class BGExtract(object): except TypeError: log.warn(u'Illegal verse number: %s', unicode(raw_verse_num)) if clean_verse_num: - verse_text = raw_verse_num.next - part = raw_verse_num.next.next + verse_text = raw_verse_num.next_element + part = raw_verse_num.next_element.next_element while not (isinstance(part, Tag) and part.get(u'class') == u'versenum'): # While we are still in the same verse grab all the text. if isinstance(part, NavigableString): verse_text += part - if isinstance(part.next, Tag) and part.next.name == u'div': + if isinstance(part.next_element, Tag) and part.next_element.name == u'div': # Run out of verses so stop. break - part = part.next + part = part.next_element verse_list[clean_verse_num] = unicode(verse_text) return verse_list @@ -230,7 +235,7 @@ class BGExtract(object): log.debug(u'BGExtract.get_bible_chapter("%s", "%s", "%s")', version, book_name, chapter) url_book_name = urllib.quote(book_name.encode("utf-8")) url_params = u'search=%s+%s&version=%s' % (url_book_name, chapter, version) - cleaner = [(re.compile(' |
|\'\+\''), lambda match: '')] + cleaner = [(CLEANER_REGEX, lambda match: '')] soup = get_soup_for_bible_ref( u'http://www.biblegateway.com/passage/?%s' % url_params, pre_parse_regex=r'', pre_parse_substitute='', cleaner=cleaner) @@ -238,7 +243,7 @@ class BGExtract(object): return None div = soup.find('div', 'result-text-style-normal') self._clean_soup(div) - span_list = div.findAll('span', 'text') + span_list = div.find_all('span', 'text') log.debug('Span list: %s', span_list) if not span_list: # If we don't get any spans then we must have the old HTML format @@ -282,7 +287,7 @@ class BGExtract(object): self.application.process_events() content = soup.find(u'table', u'infotable') if content: - content = content.findAll(u'tr') + content = content.find_all(u'tr') if not content: log.error(u'No books found in the Biblegateway response.') send_error_message(u'parse') @@ -341,19 +346,17 @@ class BSExtract(object): log.error(u'No verses found in the Bibleserver response.') send_error_message(u'parse') return None - content = content.find(u'div').findAll(u'div') - verse_number = re.compile(r'v(\d{1,2})(\d{3})(\d{3}) verse.*') + content = content.find(u'div').find_all(u'div') verses = {} for verse in content: self.application.process_events() - versenumber = int(verse_number.sub(r'\3', verse[u'class'])) + versenumber = int(VERSE_NUMBER_REGEX.sub(r'\3', verse[u'class'])) verses[versenumber] = verse.contents[1].rstrip(u'\n') return SearchResults(book_name, chapter, verses) def get_books_from_http(self, version): """ - Load a list of all books a Bible contains from Bibleserver mobile - website. + Load a list of all books a Bible contains from Bibleserver mobile website. ``version`` The version of the Bible like NIV for New International Version @@ -369,7 +372,7 @@ class BSExtract(object): log.error(u'No books found in the Bibleserver response.') send_error_message(u'parse') return None - content = content.findAll(u'li') + content = content.find_all(u'li') return [book.contents[0].contents[0] for book in content] @@ -404,14 +407,12 @@ class CWExtract(object): if not soup: return None self.application.process_events() - html_verses = soup.findAll(u'span', u'versetext') + html_verses = soup.find_all(u'span', u'versetext') if not html_verses: log.error(u'No verses found in the CrossWalk response.') send_error_message(u'parse') return None verses = {} - reduce_spaces = re.compile(r'[ ]{2,}') - fix_punctuation = re.compile(r'[ ]+([.,;])') for verse in html_verses: self.application.process_events() verse_number = int(verse.contents[0].contents[0]) @@ -432,11 +433,10 @@ class CWExtract(object): if isinstance(subsub, NavigableString): verse_text += subsub self.application.process_events() - # Fix up leading and trailing spaces, multiple spaces, and spaces - # between text and , and . + # Fix up leading and trailing spaces, multiple spaces, and spaces between text and , and . verse_text = verse_text.strip(u'\n\r\t ') - verse_text = reduce_spaces.sub(u' ', verse_text) - verse_text = fix_punctuation.sub(r'\1', verse_text) + verse_text = REDUCE_SPACES_REGEX.sub(u' ', verse_text) + verse_text = FIX_PUNKCTUATION_REGEX.sub(r'\1', verse_text) verses[verse_number] = verse_text return SearchResults(book_name, chapter, verses) @@ -458,7 +458,7 @@ class CWExtract(object): log.error(u'No books found in the Crosswalk response.') send_error_message(u'parse') return None - content = content.findAll(u'li') + content = content.find_all(u'li') books = [] for book in content: book = book.find(u'a') @@ -481,9 +481,8 @@ class HTTPBible(BibleDB): def __init__(self, parent, **kwargs): """ - Finds all the bibles defined for the system - Creates an Interface Object for each bible containing connection - information + Finds all the bibles defined for the system. Creates an Interface Object for each bible containing connection + information. Throws Exception if no Bibles are found. @@ -492,8 +491,7 @@ class HTTPBible(BibleDB): BibleDB.__init__(self, parent, **kwargs) self.download_source = kwargs[u'download_source'] self.download_name = kwargs[u'download_name'] - # TODO: Clean up proxy stuff. We probably want one global proxy per - # connection type (HTTP and HTTPS) at most. + # TODO: Clean up proxy stuff. We probably want one global proxy per connection type (HTTP and HTTPS) at most. self.proxy_server = None self.proxy_username = None self.proxy_password = None @@ -508,8 +506,8 @@ class HTTPBible(BibleDB): def do_import(self, bible_name=None): """ - Run the import. This method overrides the parent class method. Returns - ``True`` on success, ``False`` on failure. + Run the import. This method overrides the parent class method. Returns ``True`` on success, ``False`` on + failure. """ self.wizard.progress_bar.setMaximum(68) self.wizard.increment_progress_bar(translate('BiblesPlugin.HTTPBible', 'Registering Bible and loading books...')) @@ -549,8 +547,7 @@ class HTTPBible(BibleDB): if self.stop_import_flag: break self.wizard.increment_progress_bar(translate( - 'BiblesPlugin.HTTPBible', 'Importing %s...', - 'Importing ...') % book) + 'BiblesPlugin.HTTPBible', 'Importing %s...', 'Importing ...') % book) book_ref_id = self.get_book_ref_id_by_name(book, len(books), language_id) if not book_ref_id: log.exception(u'Importing books from %s - download name: "%s" '\ @@ -567,22 +564,19 @@ class HTTPBible(BibleDB): def get_verses(self, reference_list, show_error=True): """ - A reimplementation of the ``BibleDB.get_verses`` method, this one is - specifically for web Bibles. It first checks to see if the particular - chapter exists in the DB, and if not it pulls it from the web. If the - chapter DOES exist, it simply pulls the verses from the DB using the - ancestor method. + A reimplementation of the ``BibleDB.get_verses`` method, this one is specifically for web Bibles. It first + checks to see if the particular chapter exists in the DB, and if not it pulls it from the web. If the chapter + DOES exist, it simply pulls the verses from the DB using the ancestor method. ``reference_list`` - This is the list of references the media manager item wants. It is - a list of tuples, with the following format:: + This is the list of references the media manager item wants. It is a list of tuples, with the following + format:: (book_reference_id, chapter, start_verse, end_verse) - Therefore, when you are looking for multiple items, simply break - them up into references like this, bundle them into a list. This - function then runs through the list, and returns an amalgamated - list of ``Verse`` objects. For example:: + Therefore, when you are looking for multiple items, simply break them up into references like this, bundle + them into a list. This function then runs through the list, and returns an amalgamated list of ``Verse`` + objects. For example:: [(u'35', 1, 1, 1), (u'35', 2, 2, 3)] """ @@ -643,7 +637,7 @@ class HTTPBible(BibleDB): Return the number of chapters in a particular book. ``book`` - The book object to get the chapter count for. + The book object to get the chapter count for. """ log.debug(u'HTTPBible.get_chapter_count("%s")', book.name) return BiblesResourcesDB.get_chapter_count(book.book_reference_id) @@ -683,8 +677,8 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, An optional HTTP header to pass to the bible web server. ``pre_parse_regex`` - A regular expression to run on the webpage. Allows manipulation of the - webpage before passing to BeautifulSoup for parsing. + A regular expression to run on the webpage. Allows manipulation of the webpage before passing to BeautifulSoup + for parsing. ``pre_parse_substitute`` The text to replace any matches to the regular expression with. @@ -704,7 +698,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, soup = None try: if cleaner: - soup = BeautifulSoup(page_source, markupMassage=cleaner) + soup = BeautifulSoup(page_source, markup=cleaner) else: soup = BeautifulSoup(page_source) except HTMLParseError: diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 4c0f69b91..86a029b71 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -79,7 +79,7 @@ MODULES = [ 'lxml', 'chardet', 'enchant', - 'BeautifulSoup', + 'bs4', 'mako', 'migrate', 'uno', From 19f9718aec7c83659eb7a8c993234207a9101816 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 5 Apr 2013 22:06:00 +0200 Subject: [PATCH 127/235] added FIXME --- openlp/plugins/bibles/lib/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 390ac1cd9..78ed40525 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -698,7 +698,8 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, soup = None try: if cleaner: - soup = BeautifulSoup(page_source, markup=cleaner) + # FIXME: markupMassage not supported. + soup = BeautifulSoup(page_source, markupMassage=cleaner) else: soup = BeautifulSoup(page_source) except HTMLParseError: From ac32b6ca653c9723eaae6e3aef8bf971cdddc710 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 5 Apr 2013 22:20:51 +0200 Subject: [PATCH 128/235] fixed missing r'' and removed argument --- openlp/plugins/bibles/lib/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 78ed40525..370216059 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -44,7 +44,7 @@ from openlp.core.utils import get_web_page from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book -CLEANER_REGEX = re.compile(' |
|\'\+\'') +CLEANER_REGEX = re.compile(r' |
|\'\+\'') FIX_PUNKCTUATION_REGEX = re.compile(r'[ ]+([.,;])') REDUCE_SPACES_REGEX = re.compile(r'[ ]{2,}') UGLY_CHARS = { @@ -699,7 +699,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, try: if cleaner: # FIXME: markupMassage not supported. - soup = BeautifulSoup(page_source, markupMassage=cleaner) + soup = BeautifulSoup(page_source)#, markupMassage=cleaner) else: soup = BeautifulSoup(page_source) except HTMLParseError: From 97c6899a31a278c42c71fdc964989c62c37f8bc5 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 6 Apr 2013 13:53:00 +0200 Subject: [PATCH 129/235] Post merge fixups: Fix a typo in wizard.py, correct some camelCase -> under_score. --- openlp/core/ui/wizard.py | 4 ++-- .../songs/forms/duplicatesongremovalform.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 2685c63d6..c38304b8f 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -219,9 +219,9 @@ class OpenLPWizard(QtGui.QWizard): self.performWizard() self.post_wizard() else: - self.custom_cage_changed(pageId) + self.custom_page_changed(pageId) - def custom_cage_changed(self, pageId): + def custom_page_changed(self, pageId): """ Called when changing to a page other than the progress page """ diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 929d6d98a..0e8ef18b7 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -66,14 +66,14 @@ class DuplicateSongRemovalForm(OpenLPWizard): OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) - def customSignals(self): + def custom_signals(self): """ Song wizard specific signals. """ - self.finishButton.clicked.connect(self.onWizardExit) - self.cancelButton.clicked.connect(self.onWizardExit) + self.finish_button.clicked.connect(self.on_wizard_exit) + self.cancel_button.clicked.connect(self.on_wizard_exit) - def addCustomPages(self): + def add_custom_pages(self): """ Add song wizard specific pages. """ @@ -119,9 +119,9 @@ class DuplicateSongRemovalForm(OpenLPWizard): Song wizard localisation. """ self.setWindowTitle(translate(u'Wizard', u'Wizard')) - self.titleLabel.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui', + self.title_label.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui', u'Welcome to the Duplicate Song Removal Wizard')) - self.informationLabel.setText(translate("Wizard", + self.information_label.setText(translate("Wizard", u'This wizard will help you to remove duplicate songs from the song database. You will have a chance to ' u'review every potential duplicate song before it is deleted. So no songs will be deleted without your ' u'explicit approval.')) @@ -138,7 +138,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.review_page.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \ (self.review_current_count, self.review_total_count)) - def customPageChanged(self, page_id): + def custom_page_changed(self, page_id): """ Called when changing the wizard page. @@ -227,12 +227,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): duplicate_added = True return duplicate_added - def onWizardExit(self): + def on_wizard_exit(self): """ Once the wizard is finished, refresh the song list, since we potentially removed songs from it. """ - self.plugin.mediaItem.onSearchTextButtonClicked() + self.plugin.media_item.on_search_text_button_clicked() def setDefaults(self): """ From 6d5590d7396111d96bc6359a688fbb7fc5239121 Mon Sep 17 00:00:00 2001 From: M2j Date: Sat, 6 Apr 2013 19:17:59 +0200 Subject: [PATCH 130/235] 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 19:59:07 +0200 Subject: [PATCH 131/235] remove markupMassage --- openlp/plugins/bibles/lib/http.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 370216059..2eec3cbcd 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -235,10 +235,9 @@ class BGExtract(object): log.debug(u'BGExtract.get_bible_chapter("%s", "%s", "%s")', version, book_name, chapter) url_book_name = urllib.quote(book_name.encode("utf-8")) url_params = u'search=%s+%s&version=%s' % (url_book_name, chapter, version) - cleaner = [(CLEANER_REGEX, lambda match: '')] soup = get_soup_for_bible_ref( u'http://www.biblegateway.com/passage/?%s' % url_params, - pre_parse_regex=r'', pre_parse_substitute='', cleaner=cleaner) + pre_parse_regex=r'', pre_parse_substitute='') if not soup: return None div = soup.find('div', 'result-text-style-normal') @@ -665,8 +664,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): +def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None): """ Gets a webpage and returns a parsed and optionally cleaned soup or None. @@ -682,9 +680,6 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, ``pre_parse_substitute`` The text to replace any matches to the regular expression with. - - ``cleaner`` - An optional regex to use during webpage parsing. """ if not reference_url: return None @@ -697,11 +692,8 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, page_source = re.sub(pre_parse_regex, pre_parse_substitute, page_source) soup = None try: - if cleaner: - # FIXME: markupMassage not supported. - soup = BeautifulSoup(page_source)#, markupMassage=cleaner) - else: - soup = BeautifulSoup(page_source) + soup = BeautifulSoup(page_source) + CLEANER_REGEX.sub(u'', soup) except HTMLParseError: log.exception(u'BeautifulSoup could not parse the bible page.') if not soup: From c82d7f0e5069b9f258ecc7bcf265238e09878f4a Mon Sep 17 00:00:00 2001 From: M2j Date: Sat, 6 Apr 2013 20:21:23 +0200 Subject: [PATCH 132/235] 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 133/235] 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 134/235] 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 789439dd6b1ad7dbe62dc840a6dbe5c821bfcdcd Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 9 Apr 2013 23:32:19 +0200 Subject: [PATCH 135/235] Make wizard a little wider to fit at least two review widgets. --- .../songs/forms/duplicatesongremovalform.py | 45 ++++++++++--------- .../plugins/songs/forms/songreviewwidget.py | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 0e8ef18b7..cf0f3922f 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -65,6 +65,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.review_total_count = 0 OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) + self.setMinimumWidth(730) def custom_signals(self): """ @@ -96,18 +97,20 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.review_page.setObjectName(u'review_page') self.review_layout = QtGui.QVBoxLayout(self.review_page) self.review_layout.setObjectName(u'review_layout') - self.songs_horizontal_scroll_area = QtGui.QScrollArea(self.review_page) - self.songs_horizontal_scroll_area.setObjectName(u'songs_horizontal_scroll_area') - self.songs_horizontal_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songs_horizontal_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.songs_horizontal_scroll_area.setWidgetResizable(True) - self.songs_horizontal_songs_widget = QtGui.QWidget(self.songs_horizontal_scroll_area) - self.songs_horizontal_songs_widget.setObjectName(u'songs_horizontal_songs_widget') - self.songs_horizontal_layout = QtGui.QHBoxLayout(self.songs_horizontal_songs_widget) - self.songs_horizontal_layout.setObjectName(u'songs_horizontal_layout') - self.songs_horizontal_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) - self.songs_horizontal_scroll_area.setWidget(self.songs_horizontal_songs_widget) - self.review_layout.addWidget(self.songs_horizontal_scroll_area) + self.review_scroll_area = QtGui.QScrollArea(self.review_page) + self.review_scroll_area.setObjectName(u'review_scroll_area') + self.review_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.review_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.review_scroll_area.setWidgetResizable(True) + self.review_scroll_area_widget = QtGui.QWidget(self.review_scroll_area) + self.review_scroll_area_widget.setObjectName(u'review_scroll_area_widget') + self.review_scroll_area_layout = QtGui.QHBoxLayout(self.review_scroll_area_widget) + self.review_scroll_area_layout.setObjectName(u'review_scroll_area_layout') + self.review_scroll_area_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize) + self.review_scroll_area_layout.setMargin(0) + self.review_scroll_area_layout.setSpacing(0) + self.review_scroll_area.setWidget(self.review_scroll_area_widget) + self.review_layout.addWidget(self.review_scroll_area) self.review_page_id = self.addPage(self.review_page) # Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the #review page. @@ -286,13 +289,13 @@ class DuplicateSongRemovalForm(OpenLPWizard): log.exception(u'Could not remove directory: %s', save_path) self.plugin.manager.delete_object(Song, item_id) # Remove GUI elements for the song. - self.songs_horizontal_layout.removeWidget(song_review_widget) + self.review_scroll_area_layout.removeWidget(song_review_widget) song_review_widget.setParent(None) # Check if we only have one duplicate left: # 2 stretches + 1 SongReviewWidget = 3 # The SongReviewWidget is then at position 1. if len(self.duplicate_song_list[-1]) == 1: - self.songs_horizontal_layout.itemAt(1).widget().song_remove_button.setEnabled(False) + self.review_scroll_area_layout.itemAt(1).widget().song_remove_button.setEnabled(False) def proceed_to_next_review(self): """ @@ -301,16 +304,16 @@ class DuplicateSongRemovalForm(OpenLPWizard): # Remove last duplicate group. self.duplicate_song_list.pop() # Remove all previous elements. - for i in reversed(range(self.songs_horizontal_layout.count())): - item = self.songs_horizontal_layout.itemAt(i) + for i in reversed(range(self.review_scroll_area_layout.count())): + item = self.review_scroll_area_layout.itemAt(i) if isinstance(item, QtGui.QWidgetItem): # The order is important here, if the .setParent(None) call is done # before the .removeItem() call, a segfault occurs. widget = item.widget() - self.songs_horizontal_layout.removeItem(item) + self.review_scroll_area_layout.removeItem(item) widget.setParent(None) else: - self.songs_horizontal_layout.removeItem(item) + self.review_scroll_area_layout.removeItem(item) # Process next set of duplicates. self.process_current_duplicate_entry() @@ -325,12 +328,12 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.update_review_counter_text() # Add song elements to the UI. if len(self.duplicate_song_list) > 0: - self.songs_horizontal_layout.addStretch(1) + self.review_scroll_area_layout.addStretch(1) for duplicate in self.duplicate_song_list[-1]: song_review_widget = SongReviewWidget(self.review_page, duplicate) song_review_widget.song_remove_button_clicked.connect(self.remove_button_clicked) - self.songs_horizontal_layout.addWidget(song_review_widget) - self.songs_horizontal_layout.addStretch(1) + self.review_scroll_area_layout.addWidget(song_review_widget) + self.review_scroll_area_layout.addStretch(1) # Change next button to finish button on last review. if len(self.duplicate_song_list) == 1: self.button(QtGui.QWizard.FinishButton).show() diff --git a/openlp/plugins/songs/forms/songreviewwidget.py b/openlp/plugins/songs/forms/songreviewwidget.py index 0557a35b5..7b24563f9 100644 --- a/openlp/plugins/songs/forms/songreviewwidget.py +++ b/openlp/plugins/songs/forms/songreviewwidget.py @@ -75,7 +75,7 @@ class SongReviewWidget(QtGui.QWidget): self.song_vertical_layout.setObjectName(u'song_vertical_layout') self.song_group_box = QtGui.QGroupBox(self) self.song_group_box.setObjectName(u'song_group_box') - self.song_group_box.setFixedWidth(400) + self.song_group_box.setFixedWidth(300) self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box) self.song_group_box_layout.setObjectName(u'song_group_box_layout') self.song_info_form_layout = QtGui.QFormLayout() From 220681c900981c0aad6c7b42e476aba227ea38e4 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 14 Apr 2013 16:59:57 +0100 Subject: [PATCH 136/235] 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 9220beba786334f13eeaa0336f0806b93fe82ea8 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sun, 14 Apr 2013 23:38:40 +0200 Subject: [PATCH 137/235] Pull preview_list_widget out into separate file. Not yet changed calls to this new class. --- openlp/core/ui/listpreviewwidget.py | 174 ++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 openlp/core/ui/listpreviewwidget.py diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py new file mode 100644 index 000000000..44735c58a --- /dev/null +++ b/openlp/core/ui/listpreviewwidget.py @@ -0,0 +1,174 @@ +# -*- 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:`slidecontroller` module contains the most important part of OpenLP - the slide controller +""" + +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import ImageSource, Registry, ServiceItem + +class ListPreviewWidget(object): + def __init__(self, parent, is_live): + # Controller list view + self.is_live = is_live + self.preview_table_widget = QtGui.QTableWidget(parent) + self.preview_table_widget.setColumnCount(1) + self.preview_table_widget.horizontalHeader().setVisible(False) + self.preview_table_widget.setColumnWidth(0, parent.width()) + self.preview_table_widget.setObjectName(u'preview_table_widget') + self.preview_table_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.preview_table_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.preview_table_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.preview_table_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.preview_table_widget.setAlternatingRowColors(True) + self.service_item = ServiceItem() + self.clicked = QtCore.pyqtSignal() + self.double_clicked = QtCore.pyqtSignal() + if not self.is_live: + self.preview_table_widget.doubleClicked.connect(self.double_clicked) + self.preview_table_widget.clicked.connect(self.clicked) + + def clicked(self): + self.clicked.emit() + + def double_clicked(self): + self.double_clicked.emit() + + def get_preview_widget(self): + return self.preview_table_widget + + def set_active(self, active): + if active: + self.preview_table_widget.show() + else: + self.preview_table_widget.hide() + + def replace_service_manager_item(self, service_item, width): + self.service_item = service_item + self._refresh(width) + + def preview_size_changed(self, width, ratio): + """ + Takes care of the SlidePreview's size. Is called when one of the the + splitters is moved or when the screen size is changed. Note, that this + method is (also) called frequently from the mainwindow *paintEvent*. + """ + self.preview_table_widget.setColumnWidth(0, self.preview_table_widget.viewport().size().width()) + if self.service_item: + # Sort out songs, bibles, etc. + if self.service_item.is_text(): + self.preview_table_widget.resizeRowsToContents() + else: + # Sort out image heights. + for framenumber in range(len(self.service_item.get_frames())): + self.preview_table_widget.setRowHeight(framenumber, width / ratio) + + def _refresh(self, width, ratio): + """ + Loads a ServiceItem into the system from ServiceManager + Display the slide number passed + """ + self.preview_table_widget.clear() + self.preview_table_widget.setRowCount(0) + self.preview_table_widget.setColumnWidth(0, width) + row = 0 + text = [] + for framenumber, frame in enumerate(self.service_item.get_frames()): + self.preview_table_widget.setRowCount(self.preview_table_widget.rowCount() + 1) + item = QtGui.QTableWidgetItem() + slideHeight = 0 + if self.service_item.is_text(): + if frame[u'verseTag']: + # These tags are already translated. + verse_def = frame[u'verseTag'] + verse_def = u'%s%s' % (verse_def[0], verse_def[1:]) + two_line_def = u'%s\n%s' % (verse_def[0], verse_def[1:]) + row = two_line_def + else: + row += 1 + item.setText(frame[u'text']) + else: + label = QtGui.QLabel() + label.setMargin(4) + if self.service_item.is_media(): + label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) + else: + label.setScaledContents(True) + self.preview_table_widget.setCellWidget(framenumber, 0, label) + slideHeight = width / ratio + row += 1 + text.append(unicode(row)) + self.preview_table_widget.setItem(framenumber, 0, item) + if slideHeight: + self.preview_table_widget.setRowHeight(framenumber, slideHeight) + self.preview_table_widget.setVerticalHeaderLabels(text) + if self.service_item.is_text(): + self.preview_table_widget.resizeRowsToContents() + self.preview_table_widget.setColumnWidth(0, self.preview_table_widget.viewport().size().width()) + #stuff happens here, perhaps the setFocus has to happen later... + self.preview_table_widget.setFocus() + + def __updatePreviewSelection(self, slideno): + """ + Utility method to update the selected slide in the list. + """ + if slideno > self.preview_table_widget.rowCount(): + self.preview_table_widget.selectRow( + self.preview_table_widget.rowCount() - 1) + else: + self.__checkUpdateSelectedSlide(slideno) + + def __checkUpdateSelectedSlide(self, row): + """ + Check if this slide has been updated + """ + if row + 1 < self.preview_table_widget.rowCount(): + self.preview_table_widget.scrollToItem(self.preview_table_widget.item(row + 1, 0)) + self.preview_table_widget.selectRow(row) + + def _get_image_manager(self): + """ + Adds the image manager to the class dynamically + """ + if not hasattr(self, u'_image_manager'): + self._image_manager = Registry().get(u'image_manager') + return self._image_manager + + image_manager = property(_get_image_manager) + + def _get_main_window(self): + """ + Adds the main window to the class dynamically + """ + if not hasattr(self, u'_main_window'): + self._main_window = Registry().get(u'main_window') + return self._main_window + + main_window = property(_get_main_window) \ No newline at end of file From db00c5049a012912e47fd1457009fcd1403311f3 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 17:50:59 +0200 Subject: [PATCH 138/235] 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 379a27212cb177b245c6d0fcbb45d2bd6481796c Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 15 Apr 2013 18:33:50 +0200 Subject: [PATCH 139/235] Clean up preview widget interface a little. --- openlp/core/ui/listpreviewwidget.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 44735c58a..6f26d390b 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -70,10 +70,6 @@ class ListPreviewWidget(object): else: self.preview_table_widget.hide() - def replace_service_manager_item(self, service_item, width): - self.service_item = service_item - self._refresh(width) - def preview_size_changed(self, width, ratio): """ Takes care of the SlidePreview's size. Is called when one of the the @@ -90,11 +86,12 @@ class ListPreviewWidget(object): for framenumber in range(len(self.service_item.get_frames())): self.preview_table_widget.setRowHeight(framenumber, width / ratio) - def _refresh(self, width, ratio): + def replace_service_manager_item(self, service_item, width, ratio): """ Loads a ServiceItem into the system from ServiceManager Display the slide number passed """ + self.service_item = service_item self.preview_table_widget.clear() self.preview_table_widget.setRowCount(0) self.preview_table_widget.setColumnWidth(0, width) @@ -132,10 +129,10 @@ class ListPreviewWidget(object): if self.service_item.is_text(): self.preview_table_widget.resizeRowsToContents() self.preview_table_widget.setColumnWidth(0, self.preview_table_widget.viewport().size().width()) - #stuff happens here, perhaps the setFocus has to happen later... + #stuff happens here, perhaps the setFocus() has to happen later... self.preview_table_widget.setFocus() - def __updatePreviewSelection(self, slideno): + def update_preview_selection(self, slideno): """ Utility method to update the selected slide in the list. """ @@ -143,9 +140,9 @@ class ListPreviewWidget(object): self.preview_table_widget.selectRow( self.preview_table_widget.rowCount() - 1) else: - self.__checkUpdateSelectedSlide(slideno) + self.check_update_selected_slide(slideno) - def __checkUpdateSelectedSlide(self, row): + def check_update_selected_slide(self, row): """ Check if this slide has been updated """ From 3609d2e0e19b28bb23d4f35f6cca2f2b11d312cc Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 15 Apr 2013 19:12:47 +0200 Subject: [PATCH 140/235] Switch over to PreviewListWidget class. Untested. --- openlp/core/ui/__init__.py | 3 +- openlp/core/ui/listpreviewwidget.py | 9 +- openlp/core/ui/slidecontroller.py | 133 +++++++--------------------- 3 files changed, 43 insertions(+), 102 deletions(-) diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 49e59e4c1..77993c443 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -99,9 +99,10 @@ from shortcutlistform import ShortcutListForm from mediadockmanager import MediaDockManager from servicemanager import ServiceManager from thememanager import ThemeManager +from listpreviewwidget import ListPreviewWidget __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm'] + 'FormattingTagForm', 'ShortcutListForm', 'ListPreviewWidget'] diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 6f26d390b..178f748a7 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -32,7 +32,8 @@ The :mod:`slidecontroller` module contains the most important part of OpenLP - t from PyQt4 import QtCore, QtGui -from openlp.core.lib import ImageSource, Registry, ServiceItem +from openlp.core.lib import Registry, ServiceItem + class ListPreviewWidget(object): def __init__(self, parent, is_live): @@ -150,6 +151,12 @@ class ListPreviewWidget(object): self.preview_table_widget.scrollToItem(self.preview_table_widget.item(row + 1, 0)) self.preview_table_widget.selectRow(row) + def currentRow(self): + return self.preview_table_widget.currentRow() + + def rowCount(self): + return self.preview_table_widget.rowCount() + def _get_image_manager(self): """ Adds the image manager to the class dynamically diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index eaeebfba8..183e0a745 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -38,7 +38,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, SlideLimits, \ ServiceItemAction, Settings, Registry, UiStrings, ScreenList, build_icon, build_html, translate -from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType +from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType, ListPreviewWidget from openlp.core.lib.ui import create_action from openlp.core.utils.actions import ActionList, CategoryOrder @@ -157,18 +157,8 @@ class SlideController(DisplayController): self.controller_layout.setSpacing(0) self.controller_layout.setMargin(0) # Controller list view - self.preview_list_widget = QtGui.QTableWidget(self.controller) - self.preview_list_widget.setColumnCount(1) - self.preview_list_widget.horizontalHeader().setVisible(False) - self.preview_list_widget.setColumnWidth(0, self.controller.width()) - self.preview_list_widget.is_live = self.is_live - self.preview_list_widget.setObjectName(u'preview_list_widget') - self.preview_list_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.preview_list_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) - self.preview_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - self.preview_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.preview_list_widget.setAlternatingRowColors(True) - self.controller_layout.addWidget(self.preview_list_widget) + self.preview_widget = ListPreviewWidget(self, self.is_live) + self.controller_layout.addWidget(self.preview_widget.get_preview_widget()) # Build the full toolbar self.toolbar = OpenLPToolbar(self) size_toolbar_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) @@ -350,7 +340,7 @@ class SlideController(DisplayController): {u'key': u'O', u'configurable': True, u'text': translate('OpenLP.SlideController', 'Go to "Other"')} ] shortcuts.extend([{u'key': unicode(number)} for number in range(10)]) - self.preview_list_widget.addActions([create_action(self, + self.controller.addActions([create_action(self, u'shortcutAction_%s' % s[u'key'], text=s.get(u'text'), can_shortcuts=True, context=QtCore.Qt.WidgetWithChildrenShortcut, @@ -358,20 +348,20 @@ class SlideController(DisplayController): triggers=self._slideShortcutActivated) for s in shortcuts]) self.shortcutTimer.timeout.connect(self._slideShortcutActivated) # Signals - self.preview_list_widget.clicked.connect(self.onSlideSelected) + self.preview_widget.clicked.connect(self.onSlideSelected) if self.is_live: Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay) Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display) self.toolbar.set_widget_visible(self.loop_list, False) self.toolbar.set_widget_visible(self.wide_menu, False) else: - self.preview_list_widget.doubleClicked.connect(self.onGoLiveClick) + self.preview_widget.double_clicked.connect(self.onGoLiveClick) self.toolbar.set_widget_visible([u'editSong'], False) if self.is_live: self.setLiveHotkeys(self) - self.__addActionsToWidget(self.preview_list_widget) + self.__addActionsToWidget(self.controller) else: - self.preview_list_widget.addActions([self.nextItem, self.previous_item]) + self.controller.addActions([self.nextItem, self.previous_item]) Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop) Registry().register_function(u'slidecontroller_%s_next' % self.type_prefix, self.on_slide_selected_next) Registry().register_function(u'slidecontroller_%s_previous' % self.type_prefix, self.on_slide_selected_previous) @@ -427,7 +417,7 @@ class SlideController(DisplayController): if len(matches) == 1: self.shortcutTimer.stop() self.current_shortcut = u'' - self.__checkUpdateSelectedSlide(self.slideList[matches[0]]) + self.preview_widget.check_update_selected_slide(self.slideList[matches[0]]) self.slideSelected() elif sender_name != u'shortcutTimer': # Start the time as we did not have any match. @@ -437,7 +427,7 @@ class SlideController(DisplayController): if self.current_shortcut in keys: # We had more than one match for example "V1" and "V10", but # "V1" was the slide we wanted to go. - self.__checkUpdateSelectedSlide(self.slideList[self.current_shortcut]) + self.preview_widget.check_update_selected_slide(self.slideList[self.current_shortcut]) self.slideSelected() # Reset the shortcut. self.current_shortcut = u'' @@ -571,16 +561,8 @@ class SlideController(DisplayController): self.preview_display.screen = { u'size': self.preview_display.geometry()} # Make sure that the frames have the correct size. - self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width()) - if self.service_item: - # Sort out songs, bibles, etc. - if self.service_item.is_text(): - self.preview_list_widget.resizeRowsToContents() - else: - # Sort out image heights. - width = self.main_window.controlSplitter.sizes()[self.split] - for framenumber in range(len(self.service_item.get_frames())): - self.preview_list_widget.setRowHeight(framenumber, width / self.ratio) + width = self.main_window.controlSplitter.sizes()[self.split] + self.preview_widget.preview_size_changed(width, self.ratio) self.onControllerSizeChanged(self.controller.width()) def onControllerSizeChanged(self, width): @@ -706,7 +688,7 @@ class SlideController(DisplayController): Replacement item following a remote edit """ if item == self.service_item: - self._processItem(item, self.preview_list_widget.currentRow()) + self._processItem(item, self.preview_widget.currentRow()) def addServiceManagerItem(self, item, slideno): """ @@ -722,7 +704,7 @@ class SlideController(DisplayController): slidenum = 0 # If service item is the same as the current one, only change slide if slideno >= 0 and item == self.service_item: - self.__checkUpdateSelectedSlide(slidenum) + self.preview_widget.check_update_selected_slide(slidenum) self.slideSelected() else: self._processItem(item, slidenum) @@ -749,10 +731,6 @@ class SlideController(DisplayController): self._resetBlank() Registry().execute(u'%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slideno]) self.slideList = {} - width = self.main_window.controlSplitter.sizes()[self.split] - self.preview_list_widget.clear() - self.preview_list_widget.setRowCount(0) - self.preview_list_widget.setColumnWidth(0, width) if self.is_live: self.song_menu.menu().clear() self.display.audio_player.reset() @@ -777,9 +755,8 @@ class SlideController(DisplayController): self.setAudioItemsVisibility(True) row = 0 text = [] + width = self.main_window.controlSplitter.sizes()[self.split] for framenumber, frame in enumerate(self.service_item.get_frames()): - self.preview_list_widget.setRowCount(self.preview_list_widget.rowCount() + 1) - item = QtGui.QTableWidgetItem() slideHeight = 0 if self.service_item.is_text(): if frame[u'verseTag']: @@ -795,37 +772,12 @@ class SlideController(DisplayController): else: row += 1 self.slideList[unicode(row)] = row - 1 - item.setText(frame[u'text']) else: - label = QtGui.QLabel() - label.setMargin(4) - if service_item.is_media(): - label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - else: - label.setScaledContents(True) - if self.service_item.is_command(): - label.setPixmap(QtGui.QPixmap(frame[u'image'])) - else: - # If current slide set background to image - if framenumber == slideno: - self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'], - ImageSource.ImagePlugin) - image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin) - label.setPixmap(QtGui.QPixmap.fromImage(image)) - self.preview_list_widget.setCellWidget(framenumber, 0, label) slideHeight = width * (1 / self.ratio) row += 1 self.slideList[unicode(row)] = row - 1 - text.append(unicode(row)) - self.preview_list_widget.setItem(framenumber, 0, item) - if slideHeight: - self.preview_list_widget.setRowHeight(framenumber, slideHeight) - self.preview_list_widget.setVerticalHeaderLabels(text) - if self.service_item.is_text(): - self.preview_list_widget.resizeRowsToContents() - self.preview_list_widget.setColumnWidth(0, - self.preview_list_widget.viewport().size().width()) - self.__updatePreviewSelection(slideno) + self.preview_widget.update_preview_selection(slideno) + self.preview_widget.replace_service_manager_item(self.service_item, width, self.ratio) self.enableToolBar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme @@ -835,7 +787,6 @@ class SlideController(DisplayController): if service_item.is_media(): self.onMediaStart(service_item) self.slideSelected(True) - self.preview_list_widget.setFocus() if old_item: # Close the old item after the new one is opened # This avoids the service theme/desktop flashing on screen @@ -847,16 +798,6 @@ class SlideController(DisplayController): self.onMediaClose() Registry().execute(u'slidecontroller_%s_started' % self.type_prefix, [service_item]) - def __updatePreviewSelection(self, slideno): - """ - Utility method to update the selected slide in the list. - """ - if slideno > self.preview_list_widget.rowCount(): - self.preview_list_widget.selectRow( - self.preview_list_widget.rowCount() - 1) - else: - self.__checkUpdateSelectedSlide(slideno) - # Screen event methods def on_slide_selected_index(self, message): """ @@ -869,7 +810,7 @@ class SlideController(DisplayController): Registry().execute(u'%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) self.updatePreview() else: - self.__checkUpdateSelectedSlide(index) + self.preview_widget.check_update_selected_slide(index) self.slideSelected() def mainDisplaySetBackground(self): @@ -1012,9 +953,9 @@ class SlideController(DisplayController): Generate the preview when you click on a slide. if this is the Live Controller also display on the screen """ - row = self.preview_list_widget.currentRow() + row = self.preview_widget.currentRow() self.selected_row = 0 - if -1 < row < self.preview_list_widget.rowCount(): + if -1 < row < self.preview_widget.rowCount(): if self.service_item.is_command(): if self.is_live and not start: Registry().execute(u'%s_slide' % self.service_item.name.lower(), @@ -1032,7 +973,7 @@ class SlideController(DisplayController): self.service_item.bg_image_bytes = None self.updatePreview() self.selected_row = row - self.__checkUpdateSelectedSlide(row) + self.preview_widget.check_update_selected_slide(row) Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) self.display.setFocus() @@ -1040,7 +981,7 @@ class SlideController(DisplayController): """ The slide has been changed. Update the slidecontroller accordingly """ - self.__checkUpdateSelectedSlide(row) + self.preview_widget.check_update_selected_slide(row) self.updatePreview() Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) @@ -1085,8 +1026,8 @@ class SlideController(DisplayController): if self.service_item.is_command() and self.is_live: self.updatePreview() else: - row = self.preview_list_widget.currentRow() + 1 - if row == self.preview_list_widget.rowCount(): + row = self.preview_widget.currentRow() + 1 + if row == self.preview_widget.rowCount(): if wrap is None: if self.slide_limits == SlideLimits.Wrap: row = 0 @@ -1094,12 +1035,12 @@ class SlideController(DisplayController): self.serviceNext() return else: - row = self.preview_list_widget.rowCount() - 1 + row = self.preview_widget.rowCount() - 1 elif wrap: row = 0 else: - row = self.preview_list_widget.rowCount() - 1 - self.__checkUpdateSelectedSlide(row) + row = self.preview_widget.rowCount() - 1 + self.preview_widget.check_update_selected_slide(row) self.slideSelected() def on_slide_selected_previous(self): @@ -1112,27 +1053,19 @@ class SlideController(DisplayController): if self.service_item.is_command() and self.is_live: self.updatePreview() else: - row = self.preview_list_widget.currentRow() - 1 + row = self.preview_widget.currentRow() - 1 if row == -1: if self.slide_limits == SlideLimits.Wrap: - row = self.preview_list_widget.rowCount() - 1 + row = self.preview_widget.rowCount() - 1 elif self.is_live and self.slide_limits == SlideLimits.Next: self.keypress_queue.append(ServiceItemAction.PreviousLastSlide) self._process_queue() return else: row = 0 - self.__checkUpdateSelectedSlide(row) + self.preview_widget.check_update_selected_slide(row) self.slideSelected() - def __checkUpdateSelectedSlide(self, row): - """ - Check if this slide has been updated - """ - if row + 1 < self.preview_list_widget.rowCount(): - self.preview_list_widget.scrollToItem(self.preview_list_widget.item(row + 1, 0)) - self.preview_list_widget.selectRow(row) - def onToggleLoop(self): """ Toggles the loop state. @@ -1147,7 +1080,7 @@ class SlideController(DisplayController): """ Start the timer loop running and store the timer id """ - if self.preview_list_widget.rowCount() > 1: + if self.preview_widget.rowCount() > 1: self.timer_id = self.startTimer(int(self.delay_spin_box.value()) * 1000) def on_stop_loop(self): @@ -1257,8 +1190,8 @@ class SlideController(DisplayController): """ If preview copy slide item to live controller from Preview Controller """ - row = self.preview_list_widget.currentRow() - if -1 < row < self.preview_list_widget.rowCount(): + row = self.preview_widget.currentRow() + if -1 < row < self.preview_widget.rowCount(): if self.service_item.from_service: self.service_manager.preview_live(self.service_item.unique_identifier, row) else: From 0dbda9ee21514ecf7c0c9f60f7641a8c5357d54a Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 15 Apr 2013 19:26:18 +0200 Subject: [PATCH 141/235] Accidentially removed image load logic. Readded. --- openlp/core/ui/listpreviewwidget.py | 16 +++++++++++++--- openlp/core/ui/slidecontroller.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 178f748a7..d455a7477 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -32,7 +32,7 @@ The :mod:`slidecontroller` module contains the most important part of OpenLP - t from PyQt4 import QtCore, QtGui -from openlp.core.lib import Registry, ServiceItem +from openlp.core.lib import ImageSource, Registry, ServiceItem class ListPreviewWidget(object): @@ -87,7 +87,7 @@ class ListPreviewWidget(object): for framenumber in range(len(self.service_item.get_frames())): self.preview_table_widget.setRowHeight(framenumber, width / ratio) - def replace_service_manager_item(self, service_item, width, ratio): + def replace_service_manager_item(self, service_item, width, ratio, slideno): """ Loads a ServiceItem into the system from ServiceManager Display the slide number passed @@ -119,6 +119,15 @@ class ListPreviewWidget(object): label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) else: label.setScaledContents(True) + if self.service_item.is_command(): + label.setPixmap(QtGui.QPixmap(frame[u'image'])) + else: + # If current slide set background to image + if framenumber == slideno: + self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'], + ImageSource.ImagePlugin) + image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin) + label.setPixmap(QtGui.QPixmap.fromImage(image)) self.preview_table_widget.setCellWidget(framenumber, 0, label) slideHeight = width / ratio row += 1 @@ -175,4 +184,5 @@ class ListPreviewWidget(object): self._main_window = Registry().get(u'main_window') return self._main_window - main_window = property(_get_main_window) \ No newline at end of file + main_window = property(_get_main_window) + diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 183e0a745..15ba53b2f 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -36,7 +36,7 @@ from collections import deque from PyQt4 import QtCore, QtGui -from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, SlideLimits, \ +from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, SlideLimits, \ ServiceItemAction, Settings, Registry, UiStrings, ScreenList, build_icon, build_html, translate from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType, ListPreviewWidget from openlp.core.lib.ui import create_action From 7f78250bb2798cea89349febf332c5fcf1c1edfb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 15 Apr 2013 18:55:50 +0100 Subject: [PATCH 142/235] 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 fd22d17d371069e2a690131d787220c1b97f9dbc Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 15 Apr 2013 20:01:59 +0200 Subject: [PATCH 143/235] Fix an import problem and use signals correctly. Worksbzr diff --- openlp/core/ui/__init__.py | 3 +-- openlp/core/ui/listpreviewwidget.py | 17 +++++++++-------- openlp/core/ui/slidecontroller.py | 5 +++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 77993c443..49e59e4c1 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -99,10 +99,9 @@ from shortcutlistform import ShortcutListForm from mediadockmanager import MediaDockManager from servicemanager import ServiceManager from thememanager import ThemeManager -from listpreviewwidget import ListPreviewWidget __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm', 'ListPreviewWidget'] + 'FormattingTagForm', 'ShortcutListForm'] diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index d455a7477..d1639b742 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -35,9 +35,12 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import ImageSource, Registry, ServiceItem -class ListPreviewWidget(object): +class ListPreviewWidget(QtCore.QObject): + clicked = QtCore.pyqtSignal() + double_clicked = QtCore.pyqtSignal() + def __init__(self, parent, is_live): - # Controller list view + super(QtCore.QObject, self).__init__() self.is_live = is_live self.preview_table_widget = QtGui.QTableWidget(parent) self.preview_table_widget.setColumnCount(1) @@ -50,16 +53,14 @@ class ListPreviewWidget(object): self.preview_table_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.preview_table_widget.setAlternatingRowColors(True) self.service_item = ServiceItem() - self.clicked = QtCore.pyqtSignal() - self.double_clicked = QtCore.pyqtSignal() if not self.is_live: - self.preview_table_widget.doubleClicked.connect(self.double_clicked) - self.preview_table_widget.clicked.connect(self.clicked) + self.preview_table_widget.doubleClicked.connect(self._double_clicked) + self.preview_table_widget.clicked.connect(self._clicked) - def clicked(self): + def _clicked(self): self.clicked.emit() - def double_clicked(self): + def _double_clicked(self): self.double_clicked.emit() def get_preview_widget(self): diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 15ba53b2f..8492b69bb 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -38,9 +38,10 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, SlideLimits, \ ServiceItemAction, Settings, Registry, UiStrings, ScreenList, build_icon, build_html, translate -from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType, ListPreviewWidget +from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType from openlp.core.lib.ui import create_action from openlp.core.utils.actions import ActionList, CategoryOrder +from openlp.core.ui.listpreviewwidget import ListPreviewWidget log = logging.getLogger(__name__) @@ -777,7 +778,7 @@ class SlideController(DisplayController): row += 1 self.slideList[unicode(row)] = row - 1 self.preview_widget.update_preview_selection(slideno) - self.preview_widget.replace_service_manager_item(self.service_item, width, self.ratio) + self.preview_widget.replace_service_manager_item(self.service_item, width, self.ratio, slideno) self.enableToolBar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme From e2b8dc54f38fe3ad9f0cd448a1d03a4bddcfd507 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 21:54:27 +0200 Subject: [PATCH 144/235] fixed bs4 code; fixed regression --- openlp/plugins/bibles/lib/http.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 2eec3cbcd..44b19f857 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -99,14 +99,15 @@ class BGExtract(object): """ if isinstance(tag, NavigableString): return None, unicode(tag) - elif tag.get('class') == 'versenum' or tag.get('class') == 'versenum mid-line': + elif tag.get('class')[0] == "versenum" or tag.get('class')[0] == 'versenum mid-line': verse = unicode(tag.string).replace('[', '').replace(']', '').strip() return verse, None - elif tag.get('class') == 'chapternum': + elif tag.get('class')[0] == 'chapternum': verse = '1' return verse, None else: - verse, text = None, '' + verse = None + text = '' for child in tag.contents: c_verse, c_text = self._extract_verse(child) if c_verse: @@ -143,7 +144,8 @@ class BGExtract(object): tags = tags[::-1] current_text = '' for tag in tags: - verse, text = None, '' + verse = None + text = '' for child in tag.contents: c_verse, c_text = self._extract_verse(child) if c_verse: @@ -208,7 +210,7 @@ class BGExtract(object): if clean_verse_num: verse_text = raw_verse_num.next_element part = raw_verse_num.next_element.next_element - while not (isinstance(part, Tag) and part.get(u'class') == u'versenum'): + while not (isinstance(part, Tag) and part.get(u'class')[0] == u'versenum'): # While we are still in the same verse grab all the text. if isinstance(part, NavigableString): verse_text += part @@ -349,7 +351,7 @@ class BSExtract(object): verses = {} for verse in content: self.application.process_events() - versenumber = int(VERSE_NUMBER_REGEX.sub(r'\3', verse[u'class'])) + versenumber = int(VERSE_NUMBER_REGEX.sub(r'\3', u' '.join(verse[u'class']))) verses[versenumber] = verse.contents[1].rstrip(u'\n') return SearchResults(book_name, chapter, verses) @@ -374,6 +376,16 @@ class BSExtract(object): content = content.find_all(u'li') return [book.contents[0].contents[0] for book in content] + def _get_application(self): + """ + Adds the openlp to the class dynamically + """ + if not hasattr(self, u'_application'): + self._application = Registry().get(u'application') + return self._application + + application = property(_get_application) + class CWExtract(object): """ @@ -693,7 +705,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre soup = None try: soup = BeautifulSoup(page_source) - CLEANER_REGEX.sub(u'', soup) + CLEANER_REGEX.sub(u'', unicode(soup)) except HTMLParseError: log.exception(u'BeautifulSoup could not parse the bible page.') if not soup: From ab3ee9768bb8ce0cd4dffc9a5b812be4c0c20294 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 23:04:49 +0200 Subject: [PATCH 145/235] remove bzrlib --- openlp/core/utils/__init__.py | 59 +++++++++++------------------------ 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 0e48f2fa5..f1c150989 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -102,45 +102,24 @@ def get_application_version(): return APPLICATION_VERSION if u'--dev-version' in sys.argv or u'-d' in sys.argv: # If we're running the dev version, let's use bzr to get the version. - try: - # If bzrlib is available, use it. - from bzrlib.branch import Branch - b = Branch.open_containing('.')[0] - b.lock_read() - try: - # Get the branch's latest revision number. - revno = b.revno() - # Convert said revision number into a bzr revision id. - revision_id = b.dotted_revno_to_revision_id((revno,)) - # Get a dict of tags, with the revision id as the key. - tags = b.tags.get_reverse_tag_dict() - # Check if the latest - if revision_id in tags: - full_version = u'%s' % tags[revision_id][0] - else: - full_version = '%s-bzr%s' % (sorted(b.tags.get_tag_dict().keys())[-1], revno) - finally: - b.unlock() - except: - # Otherwise run the command line bzr client. - bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr tags') - lines = output.splitlines() - if not lines: - tag = u'0.0.0' - revision = u'0' - else: - tag, revision = lines[-1].split() - bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr log') - latest = output.split(u':')[0] - full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) + bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr tags') + lines = output.splitlines() + if not lines: + tag = u'0.0.0' + revision = u'0' + else: + tag, revision = lines[-1].split() + bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr log') + latest = output.split(u':')[0] + full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) else: # We're not running the development version, let's use the file. filepath = AppLocation.get_directory(AppLocation.VersionDir) @@ -406,7 +385,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 f0334457d0139a05b117b32707616fee40cc1e09 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 23:07:45 +0200 Subject: [PATCH 146/235] removed sqlite2 code --- openlp/plugins/bibles/lib/openlp1.py | 113 ------------ openlp/plugins/songs/lib/olp1import.py | 227 ------------------------- 2 files changed, 340 deletions(-) delete mode 100644 openlp/plugins/bibles/lib/openlp1.py delete mode 100644 openlp/plugins/songs/lib/olp1import.py diff --git a/openlp/plugins/bibles/lib/openlp1.py b/openlp/plugins/bibles/lib/openlp1.py deleted file mode 100644 index 5d96e6c26..000000000 --- a/openlp/plugins/bibles/lib/openlp1.py +++ /dev/null @@ -1,113 +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 # -############################################################################### - -import logging -import sqlite -import sys - -from openlp.core.ui.wizard import WizardStrings -from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB - -log = logging.getLogger(__name__) - -class OpenLP1Bible(BibleDB): - """ - This class provides the OpenLPv1 bible importer. - """ - def __init__(self, parent, **kwargs): - """ - Constructor. - """ - log.debug(self.__class__.__name__) - BibleDB.__init__(self, parent, **kwargs) - self.filename = kwargs[u'filename'] - - def do_import(self, bible_name=None): - """ - Imports an openlp.org v1 bible. - """ - connection = None - cursor = None - try: - connection = sqlite.connect(self.filename.encode(sys.getfilesystemencoding())) - cursor = connection.cursor() - except sqlite.DatabaseError: - log.exception(u'File "%s" is encrypted or not a sqlite database, ' - 'therefore not an openlp.org 1.x database either' % self.filename) - # Please add an user error here! - # This file is not an openlp.org 1.x bible database. - return False - #Create the bible language - language_id = self.get_language(bible_name) - if not language_id: - log.exception(u'Importing books from "%s" failed' % self.filename) - return False - # Create all books. - try: - cursor.execute(u'SELECT id, testament_id, name, abbreviation FROM book') - except sqlite.DatabaseError as error: - log.exception(u'DatabaseError: %s' % error) - # Please add an user error here! - # This file is not an openlp.org 1.x bible database. - return False - books = cursor.fetchall() - self.wizard.progress_bar.setMaximum(len(books) + 1) - for book in books: - if self.stop_import_flag: - connection.close() - return False - book_id = int(book[0]) - testament_id = int(book[1]) - name = unicode(book[2], u'cp1252') - abbreviation = unicode(book[3], u'cp1252') - book_ref_id = self.get_book_ref_id_by_name(name, len(books), - language_id) - if not book_ref_id: - log.exception(u'Importing books from "%s" failed' % self.filename) - return False - book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - db_book = self.create_book(name, book_ref_id, book_details[u'testament_id']) - # Update the progess bar. - self.wizard.increment_progress_bar(WizardStrings.ImportingType % name) - # Import the verses for this book. - cursor.execute(u'SELECT chapter, verse, text || \'\' AS text FROM ' - 'verse WHERE book_id=%s' % book_id) - verses = cursor.fetchall() - for verse in verses: - if self.stop_import_flag: - connection.close() - return False - chapter = int(verse[0]) - verse_number = int(verse[1]) - text = unicode(verse[2], u'cp1252') - self.create_verse(db_book.id, chapter, verse_number, text) - self.application.process_events() - self.session.commit() - connection.close() - return True diff --git a/openlp/plugins/songs/lib/olp1import.py b/openlp/plugins/songs/lib/olp1import.py deleted file mode 100644 index 649b8af1f..000000000 --- a/openlp/plugins/songs/lib/olp1import.py +++ /dev/null @@ -1,227 +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:`olp1import` module provides the functionality for importing -openlp.org 1.x song databases into the current installation database. -""" - -import logging -from chardet.universaldetector import UniversalDetector -import sqlite -import sys -import os - -from openlp.core.lib import Registry, translate -from openlp.plugins.songs.lib import retrieve_windows_encoding -from songimport import SongImport - -log = logging.getLogger(__name__) - -class OpenLP1SongImport(SongImport): - """ - The :class:`OpenLP1SongImport` class provides OpenLP with the ability to - import song databases from installations of openlp.org 1.x. - """ - lastEncoding = u'windows-1252' - - def __init__(self, manager, **kwargs): - """ - Initialise the import. - - ``manager`` - The song manager for the running OpenLP installation. - - ``filename`` - The database providing the data to import. - """ - SongImport.__init__(self, manager, **kwargs) - - def doImport(self): - """ - Run the import for an openlp.org 1.x song database. - """ - if not self.import_source.endswith(u'.olp'): - self.logError(self.import_source, - translate('SongsPlugin.OpenLP1SongImport', 'Not a valid openlp.org 1.x song database.')) - return - encoding = self.getEncoding() - if not encoding: - return - # Connect to the database. - connection = sqlite.connect(self.import_source, mode=0444, encoding=(encoding, 'replace')) - cursor = connection.cursor() - # Determine if the db supports linking audio to songs. - cursor.execute(u'SELECT name FROM sqlite_master ' - u'WHERE type = \'table\' AND name = \'tracks\'') - db_has_tracks = len(cursor.fetchall()) > 0 - # Determine if the db contains theme information. - cursor.execute(u'SELECT name FROM sqlite_master ' - u'WHERE type = \'table\' AND name = \'settings\'') - db_has_themes = len(cursor.fetchall()) > 0 - # "cache" our list of authors. - cursor.execute(u'-- types int, unicode') - cursor.execute(u'SELECT authorid, authorname FROM authors') - authors = cursor.fetchall() - if db_has_tracks: - # "cache" our list of tracks. - cursor.execute(u'-- types int, unicode') - cursor.execute(u'SELECT trackid, fulltrackname FROM tracks') - tracks = cursor.fetchall() - if db_has_themes: - # "cache" our list of themes. - themes = {} - cursor.execute(u'-- types int, unicode') - cursor.execute(u'SELECT settingsid, settingsname FROM settings') - for theme_id, theme_name in cursor.fetchall(): - if theme_name in self.theme_manager.get_themes(): - themes[theme_id] = theme_name - # Import the songs. - cursor.execute(u'-- types int, unicode, unicode, unicode') - cursor.execute(u'SELECT songid, songtitle, lyrics || \'\' AS ' \ - u'lyrics, copyrightinfo FROM songs') - songs = cursor.fetchall() - self.import_wizard.progress_bar.setMaximum(len(songs)) - for song in songs: - self.setDefaults() - if self.stop_import_flag: - break - song_id = song[0] - self.title = song[1] - lyrics = song[2].replace(u'\r\n', u'\n') - self.addCopyright(song[3]) - if db_has_themes: - cursor.execute(u'-- types int') - cursor.execute( - u'SELECT settingsid FROM songs WHERE songid = %s' % song_id) - theme_id = cursor.fetchone()[0] - self.themeName = themes.get(theme_id, u'') - verses = lyrics.split(u'\n\n') - for verse in verses: - if verse.strip(): - self.addVerse(verse.strip()) - cursor.execute(u'-- types int') - cursor.execute(u'SELECT authorid FROM songauthors ' - u'WHERE songid = %s' % song_id) - author_ids = cursor.fetchall() - for author_id in author_ids: - if self.stop_import_flag: - break - for author in authors: - if author[0] == author_id[0]: - self.parse_author(author[1]) - break - if self.stop_import_flag: - break - if db_has_tracks: - cursor.execute(u'-- types int, int') - cursor.execute(u'SELECT trackid, listindex ' - u'FROM songtracks ' - u'WHERE songid = %s ORDER BY listindex' % song_id) - track_ids = cursor.fetchall() - for track_id, listindex in track_ids: - if self.stop_import_flag: - break - for track in tracks: - if track[0] == track_id: - media_file = self.expandMediaFile(track[1]) - self.addMediaFile(media_file, listindex) - break - if self.stop_import_flag: - break - if not self.finish(): - self.logError(self.import_source) - - def getEncoding(self): - """ - Detect character encoding of an openlp.org 1.x song database. - """ - # Connect to the database. - connection = sqlite.connect(self.import_source.encode( - sys.getfilesystemencoding()), mode=0444) - cursor = connection.cursor() - - detector = UniversalDetector() - # Detect charset by authors. - cursor.execute(u'SELECT authorname FROM authors') - authors = cursor.fetchall() - for author in authors: - detector.feed(author[0]) - if detector.done: - detector.close() - return detector.result[u'encoding'] - # Detect charset by songs. - cursor.execute(u'SELECT songtitle, copyrightinfo, ' - u'lyrics || \'\' AS lyrics FROM songs') - songs = cursor.fetchall() - for index in [0, 1, 2]: - for song in songs: - detector.feed(song[index]) - if detector.done: - detector.close() - return detector.result[u'encoding'] - # Detect charset by songs. - cursor.execute(u'SELECT name FROM sqlite_master ' - u'WHERE type = \'table\' AND name = \'tracks\'') - if cursor.fetchall(): - cursor.execute(u'SELECT fulltrackname FROM tracks') - tracks = cursor.fetchall() - for track in tracks: - detector.feed(track[0]) - if detector.done: - detector.close() - return detector.result[u'encoding'] - detector.close() - return retrieve_windows_encoding(detector.result[u'encoding']) - - def expandMediaFile(self, filename): - """ - When you're on Windows, this function expands the file name to include - the path to OpenLP's application data directory. If you are not on - Windows, it returns the original file name. - - ``filename`` - The filename to expand. - """ - if sys.platform != u'win32' and not os.environ.get(u'ALLUSERSPROFILE') and not os.environ.get(u'APPDATA'): - return filename - common_app_data = os.path.join(os.environ[u'ALLUSERSPROFILE'], - os.path.split(os.environ[u'APPDATA'])[1]) - if not common_app_data: - return filename - return os.path.join(common_app_data, u'openlp.org', 'Audio', filename) - - def _get_theme_manager(self): - """ - Adds the theme manager to the class dynamically - """ - if not hasattr(self, u'_theme_manager'): - self._theme_manager = Registry().get(u'theme_manager') - return self._theme_manager - - theme_manager = property(_get_theme_manager) From b3d4be1a44b39cdc27114e179d549baaeef90b30 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 23:15:29 +0200 Subject: [PATCH 147/235] removed sqlite from exception form --- openlp/core/ui/exceptionform.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 6b77a8c6f..d6c8ad575 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -59,11 +59,6 @@ try: ENCHANT_VERSION = enchant.__version__ except ImportError: ENCHANT_VERSION = u'-' -try: - import sqlite - SQLITE_VERSION = sqlite.version -except ImportError: - SQLITE_VERSION = u'-' try: import mako MAKO_VERSION = mako.__version__ @@ -149,7 +144,6 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog): u'lxml: %s\n' % etree.__version__ + \ u'Chardet: %s\n' % CHARDET_VERSION + \ 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 + \ From 7cae10917d07d5364398f4ac3aba96f76a708f1d Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 23:15:56 +0200 Subject: [PATCH 148/235] removed sqlite from check_dependencies --- scripts/check_dependencies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index a6e075db4..941f6bdc9 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -88,7 +88,6 @@ MODULES = [ OPTIONAL_MODULES = [ - ('sqlite', ' (SQLite 2 support)'), ('MySQLdb', ' (MySQL support)'), ('psycopg2', ' (PostgreSQL support)'), ('nose', ' (testing framework)'), From 8d5aaa927daa61e9fba67edac9afcf3a88deb1d0 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Mon, 15 Apr 2013 23:31:04 +0200 Subject: [PATCH 149/235] removed import from gui --- openlp/core/lib/uistrings.py | 1 - openlp/core/ui/wizard.py | 2 - .../plugins/bibles/forms/bibleimportform.py | 55 +----------------- openlp/plugins/bibles/lib/manager.py | 13 +---- openlp/plugins/songs/lib/importer.py | 56 +++++++------------ 5 files changed, 24 insertions(+), 103 deletions(-) diff --git a/openlp/core/lib/uistrings.py b/openlp/core/lib/uistrings.py index a5a45961a..6d4b4d250 100644 --- a/openlp/core/lib/uistrings.py +++ b/openlp/core/lib/uistrings.py @@ -107,7 +107,6 @@ class UiStrings(object): self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural') self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular') self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural') - self.OLPV1 = translate('OpenLP.Ui', 'openlp.org 1.x') self.OLPV2 = translate('OpenLP.Ui', 'OpenLP 2') self.OLPV2x = translate('OpenLP.Ui', 'OpenLP 2.1') self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?') diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index f8b32a9c0..783b310b3 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -58,8 +58,6 @@ class WizardStrings(object): ImportingType = translate('OpenLP.Ui', 'Importing "%s"...') ImportSelect = translate('OpenLP.Ui', 'Select Import Source') ImportSelectLong = translate('OpenLP.Ui', 'Select the import format and the location to import from.') - NoSqlite = translate('OpenLP.Ui', 'The openlp.org 1.x importer has been disabled due to a missing Python module. ' - 'If you want to use this importer, you will need to install the "python-sqlite" module.') OpenTypeFile = translate('OpenLP.Ui', 'Open %s File') OpenTypeFolder = translate('OpenLP.Ui', 'Open %s Folder') PercentSymbolFormat = translate('OpenLP.Ui', '%p%') diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index f8d771e77..847b2b7a2 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -101,12 +101,6 @@ class BibleImportForm(OpenLPWizard): """ Perform any custom initialisation for bible importing. """ - if BibleFormat.get_availability(BibleFormat.OpenLP1): - self.openlp1DisabledLabel.hide() - else: - self.openlp1FileLabel.hide() - self.openlp1FileEdit.hide() - self.openlp1BrowseButton.hide() self.manager.set_process_dialog(self) self.loadWebBibles() self.restart() @@ -121,7 +115,6 @@ class BibleImportForm(OpenLPWizard): self.csvBooksButton.clicked.connect(self.onCsvBooksBrowseButtonClicked) self.csvVersesButton.clicked.connect(self.onCsvVersesBrowseButtonClicked) self.openSongBrowseButton.clicked.connect(self.onOpenSongBrowseButtonClicked) - self.openlp1BrowseButton.clicked.connect(self.onOpenlp1BrowseButtonClicked) def add_custom_pages(self): """ @@ -259,29 +252,6 @@ class BibleImportForm(OpenLPWizard): self.webProxyLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.webPasswordEdit) self.webTabWidget.addTab(self.webProxyTab, u'') self.selectStack.addWidget(self.webTabWidget) - self.openlp1Widget = QtGui.QWidget(self.selectPage) - self.openlp1Widget.setObjectName(u'Openlp1Widget') - self.openlp1Layout = QtGui.QFormLayout(self.openlp1Widget) - self.openlp1Layout.setMargin(0) - self.openlp1Layout.setObjectName(u'Openlp1Layout') - self.openlp1FileLabel = QtGui.QLabel(self.openlp1Widget) - self.openlp1FileLabel.setObjectName(u'Openlp1FileLabel') - self.openlp1FileLayout = QtGui.QHBoxLayout() - self.openlp1FileLayout.setObjectName(u'Openlp1FileLayout') - self.openlp1FileEdit = QtGui.QLineEdit(self.openlp1Widget) - self.openlp1FileEdit.setObjectName(u'Openlp1FileEdit') - self.openlp1FileLayout.addWidget(self.openlp1FileEdit) - self.openlp1BrowseButton = QtGui.QToolButton(self.openlp1Widget) - self.openlp1BrowseButton.setIcon(self.open_icon) - self.openlp1BrowseButton.setObjectName(u'Openlp1BrowseButton') - self.openlp1FileLayout.addWidget(self.openlp1BrowseButton) - self.openlp1Layout.addRow(self.openlp1FileLabel, self.openlp1FileLayout) - self.openlp1DisabledLabel = QtGui.QLabel(self.openlp1Widget) - self.openlp1DisabledLabel.setWordWrap(True) - self.openlp1DisabledLabel.setObjectName(u'Openlp1DisabledLabel') - self.openlp1Layout.addRow(self.openlp1DisabledLabel) - self.openlp1Layout.setItem(1, QtGui.QFormLayout.LabelRole, self.spacer) - self.selectStack.addWidget(self.openlp1Widget) self.selectPageLayout.addLayout(self.selectStack) self.addPage(self.selectPage) # License Page @@ -330,8 +300,6 @@ class BibleImportForm(OpenLPWizard): self.formatComboBox.setItemText(BibleFormat.OpenSong, WizardStrings.OS) self.formatComboBox.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm', 'Web Download')) - self.formatComboBox.setItemText(BibleFormat.OpenLP1, UiStrings().OLPV1) - self.openlp1FileLabel.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.osisFileLabel.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.csvBooksLabel.setText(translate('BiblesPlugin.ImportWizardForm', 'Books file:')) self.csvVersesLabel.setText(translate('BiblesPlugin.ImportWizardForm', 'Verses file:')) @@ -364,14 +332,12 @@ class BibleImportForm(OpenLPWizard): 'Please wait while your Bible is imported.')) self.progress_label.setText(WizardStrings.Ready) self.progress_bar.setFormat(u'%p%') - self.openlp1DisabledLabel.setText(WizardStrings.NoSqlite) # Align all QFormLayouts towards each other. labelWidth = max(self.formatLabel.minimumSizeHint().width(), self.osisFileLabel.minimumSizeHint().width(), self.csvBooksLabel.minimumSizeHint().width(), self.csvVersesLabel.minimumSizeHint().width(), - self.openSongFileLabel.minimumSizeHint().width(), - self.openlp1FileLabel.minimumSizeHint().width()) + self.openSongFileLabel.minimumSizeHint().width()) self.spacer.changeSize(labelWidth, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) def validateCurrentPage(self): @@ -406,11 +372,6 @@ class BibleImportForm(OpenLPWizard): elif self.field(u'source_format') == BibleFormat.WebDownload: self.versionNameEdit.setText(self.webTranslationComboBox.currentText()) return True - elif self.field(u'source_format') == BibleFormat.OpenLP1: - if not self.field(u'openlp1_location'): - critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % UiStrings().OLPV1) - self.openlp1FileEdit.setFocus() - return False return True elif self.currentPage() == self.licenseDetailsPage: license_version = self.field(u'license_version') @@ -484,13 +445,6 @@ class BibleImportForm(OpenLPWizard): """ self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.OS, self.openSongFileEdit, u'last directory import') - def onOpenlp1BrowseButtonClicked(self): - """ - Show the file open dialog for the openlp.org 1.x file. - """ - self.get_file_name(WizardStrings.OpenTypeFile % UiStrings().OLPV1, self.openlp1FileEdit, u'last directory import', - u'%s (*.bible)' % translate('BiblesPlugin.ImportWizardForm', 'openlp.org 1.x Bible Files')) - def register_fields(self): """ Register the bible import wizard fields. @@ -505,7 +459,6 @@ class BibleImportForm(OpenLPWizard): self.selectPage.registerField(u'proxy_server', self.webServerEdit) self.selectPage.registerField(u'proxy_username', self.webUserEdit) self.selectPage.registerField(u'proxy_password', self.webPasswordEdit) - self.selectPage.registerField(u'openlp1_location', self.openlp1FileEdit) self.licenseDetailsPage.registerField(u'license_version', self.versionNameEdit) self.licenseDetailsPage.registerField(u'license_copyright', self.copyrightEdit) self.licenseDetailsPage.registerField(u'license_permissions', self.permissionsEdit) @@ -615,12 +568,6 @@ class BibleImportForm(OpenLPWizard): proxy_username=self.field(u'proxy_username'), proxy_password=self.field(u'proxy_password') ) - elif bible_type == BibleFormat.OpenLP1: - # Import an openlp.org 1.x bible. - importer = self.manager.import_bible(BibleFormat.OpenLP1, - name=license_version, - filename=self.field(u'openlp1_location') - ) if importer.do_import(license_version): self.manager.save_meta_data(license_version, license_version, license_copyright, license_permissions) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 15c2e5d80..abcfa7ddc 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -38,12 +38,8 @@ from csvbible import CSVBible from http import HTTPBible from opensong import OpenSongBible from osis import OSISBible -# Imports that might fail. -try: - from openlp1 import OpenLP1Bible - HAS_OPENLP1 = True -except ImportError: - HAS_OPENLP1 = False + + log = logging.getLogger(__name__) @@ -59,7 +55,6 @@ class BibleFormat(object): CSV = 1 OpenSong = 2 WebDownload = 3 - OpenLP1 = 4 @staticmethod def get_class(format): @@ -77,8 +72,6 @@ class BibleFormat(object): return OpenSongBible elif format == BibleFormat.WebDownload: return HTTPBible - elif format == BibleFormat.OpenLP1: - return OpenLP1Bible else: return None @@ -92,7 +85,6 @@ class BibleFormat(object): BibleFormat.CSV, BibleFormat.OpenSong, BibleFormat.WebDownload, - BibleFormat.OpenLP1 ] @staticmethod @@ -463,6 +455,5 @@ class BibleManager(object): main_window = property(_get_main_window) -BibleFormat.set_availability(BibleFormat.OpenLP1, HAS_OPENLP1) __all__ = [u'BibleFormat'] diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index ff018abf3..76b3e0a70 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -50,13 +50,11 @@ from sundayplusimport import SundayPlusImport from foilpresenterimport import FoilPresenterImport from zionworximport import ZionWorxImport # Imports that might fail + + log = logging.getLogger(__name__) -try: - from olp1import import OpenLP1SongImport - HAS_OPENLP1 = True -except ImportError: - log.exception('Error importing %s', 'OpenLP1SongImport') - HAS_OPENLP1 = False + + try: from sofimport import SofImport HAS_SOF = True @@ -144,23 +142,22 @@ class SongFormat(object): Unknown = -1 OpenLyrics = 0 OpenLP2 = 1 - OpenLP1 = 2 - Generic = 3 - CCLI = 4 - DreamBeam = 5 - EasySlides = 6 - EasyWorship = 7 - FoilPresenter = 8 - MediaShout = 9 - OpenSong = 10 - PowerSong = 11 - SongBeamer = 12 - SongPro = 13 - SongShowPlus = 14 - SongsOfFellowship = 15 - SundayPlus = 16 - WordsOfWorship = 17 - ZionWorx = 18 + Generic = 2 + CCLI = 3 + DreamBeam = 4 + EasySlides = 5 + EasyWorship = 6 + FoilPresenter = 7 + MediaShout = 8 + OpenSong = 9 + PowerSong = 10 + SongBeamer = 11 + SongPro = 12 + SongShowPlus = 13 + SongsOfFellowship = 14 + SundayPlus = 15 + WordsOfWorship = 16 + ZionWorx = 17 # Set optional attribute defaults __defaults__ = { @@ -191,14 +188,6 @@ class SongFormat(object): u'selectMode': SongFormatSelect.SingleFile, u'filter': u'%s (*.sqlite)' % (translate('SongsPlugin.ImportWizardForm', 'OpenLP 2.0 Databases')) }, - OpenLP1: { - u'name': UiStrings().OLPV1, - u'prefix': u'openLP1', - u'canDisable': True, - u'selectMode': SongFormatSelect.SingleFile, - u'filter': u'%s (*.olp)' % translate('SongsPlugin.ImportWizardForm', 'openlp.org v1.x Databases'), - u'disabledLabelText': WizardStrings.NoSqlite - }, Generic: { u'name': translate('SongsPlugin.ImportWizardForm', 'Generic Document/Presentation'), u'prefix': u'generic', @@ -332,7 +321,6 @@ class SongFormat(object): return [ SongFormat.OpenLyrics, SongFormat.OpenLP2, - SongFormat.OpenLP1, SongFormat.Generic, SongFormat.CCLI, SongFormat.DreamBeam, @@ -387,9 +375,7 @@ class SongFormat(object): """ SongFormat.__attributes__[format][attribute] = value -SongFormat.set(SongFormat.OpenLP1, u'availability', HAS_OPENLP1) -if HAS_OPENLP1: - SongFormat.set(SongFormat.OpenLP1, u'class', OpenLP1SongImport) + SongFormat.set(SongFormat.SongsOfFellowship, u'availability', HAS_SOF) if HAS_SOF: SongFormat.set(SongFormat.SongsOfFellowship, u'class', SofImport) From 140178a67404db4f9b065bcc603d13f898da4146 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 16 Apr 2013 17:55:23 +0200 Subject: [PATCH 150/235] added code back which should not be removed in this proposal --- openlp/core/utils/__init__.py | 59 ++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index f1c150989..0e48f2fa5 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -102,24 +102,45 @@ def get_application_version(): return APPLICATION_VERSION if u'--dev-version' in sys.argv or u'-d' in sys.argv: # If we're running the dev version, let's use bzr to get the version. - bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr tags') - lines = output.splitlines() - if not lines: - tag = u'0.0.0' - revision = u'0' - else: - tag, revision = lines[-1].split() - bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr log') - latest = output.split(u':')[0] - full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) + try: + # If bzrlib is available, use it. + from bzrlib.branch import Branch + b = Branch.open_containing('.')[0] + b.lock_read() + try: + # Get the branch's latest revision number. + revno = b.revno() + # Convert said revision number into a bzr revision id. + revision_id = b.dotted_revno_to_revision_id((revno,)) + # Get a dict of tags, with the revision id as the key. + tags = b.tags.get_reverse_tag_dict() + # Check if the latest + if revision_id in tags: + full_version = u'%s' % tags[revision_id][0] + else: + full_version = '%s-bzr%s' % (sorted(b.tags.get_tag_dict().keys())[-1], revno) + finally: + b.unlock() + except: + # Otherwise run the command line bzr client. + bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr tags') + lines = output.splitlines() + if not lines: + tag = u'0.0.0' + revision = u'0' + else: + tag, revision = lines[-1].split() + bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr log') + latest = output.split(u':')[0] + full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) else: # We're not running the development version, let's use the file. filepath = AppLocation.get_directory(AppLocation.VersionDir) @@ -385,7 +406,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 e4d3d8d6d2d9444dd85d49c5a2df713f1f773ec9 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 16 Apr 2013 19:33:36 +0200 Subject: [PATCH 151/235] Rename a slideno -> row for more consistency. --- openlp/core/ui/listpreviewwidget.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index d1639b742..3e9bc065c 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -143,15 +143,14 @@ class ListPreviewWidget(QtCore.QObject): #stuff happens here, perhaps the setFocus() has to happen later... self.preview_table_widget.setFocus() - def update_preview_selection(self, slideno): + def update_preview_selection(self, row): """ Utility method to update the selected slide in the list. """ - if slideno > self.preview_table_widget.rowCount(): - self.preview_table_widget.selectRow( - self.preview_table_widget.rowCount() - 1) + if row > self.preview_table_widget.rowCount(): + self.preview_table_widget.selectRow(self.preview_table_widget.rowCount() - 1) else: - self.check_update_selected_slide(slideno) + self.check_update_selected_slide(row) def check_update_selected_slide(self, row): """ From 1bbf52c7bfe9b537feb27ae6d7e055c2d3e0b225 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 16 Apr 2013 19:34:07 +0200 Subject: [PATCH 152/235] Fix a logic error, an array index border was wrong. --- openlp/core/ui/listpreviewwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 3e9bc065c..fb94555fd 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -147,7 +147,7 @@ class ListPreviewWidget(QtCore.QObject): """ Utility method to update the selected slide in the list. """ - if row > self.preview_table_widget.rowCount(): + if row >= self.preview_table_widget.rowCount(): self.preview_table_widget.selectRow(self.preview_table_widget.rowCount() - 1) else: self.check_update_selected_slide(row) From 01a0ad3618916401aa3d581492a12e4aaebac0a1 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 16 Apr 2013 19:48:42 +0200 Subject: [PATCH 153/235] added whitespace to avoid conflict --- 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 f1c150989..20163ae64 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -386,7 +386,7 @@ def get_natural_key(string): # 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 applocation import AppLocation From 1b447c45904ad5e731e500e6b81c3d63d21581c6 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 16 Apr 2013 19:54:40 +0200 Subject: [PATCH 154/235] reoved whitespace --- 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 20163ae64..f1c150989 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -386,7 +386,7 @@ def get_natural_key(string): # 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 applocation import AppLocation From 1e8ad4ab7e9f628be8e58610d43fc11c77b6da41 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 16 Apr 2013 19:56:06 +0200 Subject: [PATCH 155/235] fixed setup.py --- openlp/.version | 2 +- setup.py | 51 +++++++++++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/openlp/.version b/openlp/.version index d41002840..b6e0556c1 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.1.0-bzr2141 +2.0.1-bzr2233 \ No newline at end of file diff --git a/setup.py b/setup.py index a96259380..3bd3007b7 100755 --- a/setup.py +++ b/setup.py @@ -27,12 +27,15 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -from setuptools import setup, find_packages import re +from setuptools import setup, find_packages +from subprocess import Popen, PIPE + VERSION_FILE = 'openlp/.version' SPLIT_ALPHA_DIGITS = re.compile(r'(\d+|\D+)') + def try_int(s): """ Convert string s to an integer if possible. Fail silently and return @@ -46,6 +49,7 @@ def try_int(s): except Exception: return s + def natural_sort_key(s): """ Return a tuple by which s is sorted. @@ -55,6 +59,7 @@ def natural_sort_key(s): """ return map(try_int, SPLIT_ALPHA_DIGITS.findall(s)) + def natural_compare(a, b): """ Compare two strings naturally and return the result. @@ -67,6 +72,7 @@ def natural_compare(a, b): """ return cmp(natural_sort_key(a), natural_sort_key(b)) + def natural_sort(seq, compare=natural_compare): """ Returns a copy of seq, sorted by natural string sort. @@ -77,28 +83,27 @@ def natural_sort(seq, compare=natural_compare): return temp try: - # Try to import Bazaar - from bzrlib.branch import Branch - b = Branch.open_containing('.')[0] - b.lock_read() - try: - # Get the branch's latest revision number. - revno = b.revno() - # Convert said revision number into a bzr revision id. - revision_id = b.dotted_revno_to_revision_id((revno,)) - # Get a dict of tags, with the revision id as the key. - tags = b.tags.get_reverse_tag_dict() - # Check if the latest - if revision_id in tags: - version = u'%s' % tags[revision_id][0] - else: - version = '%s-bzr%s' % \ - (natural_sort(b.tags.get_tag_dict().keys())[-1], revno) - ver_file = open(VERSION_FILE, u'w') - ver_file.write(version) - ver_file.close() - finally: - b.unlock() + bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr tags') + lines = output.splitlines() + if not lines: + tag = u'0.0.0' + revision = u'0' + else: + tag, revision = lines[-1].split() + bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr log') + latest = output.split(u':')[0] + version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) + ver_file = open(VERSION_FILE, u'w') + ver_file.write(version) + ver_file.close() except: ver_file = open(VERSION_FILE, u'r') version = ver_file.read().strip() From d68728833601b2a7b98759330aae37f3338c1723 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 16 Apr 2013 23:42:23 +0200 Subject: [PATCH 156/235] Correct a preview_list_widget -> preview_widget call and fix update of preview when switching the service item. --- openlp/core/ui/servicemanager.py | 2 +- openlp/core/ui/slidecontroller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 6e0f4ad95..4aa7c5307 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1379,7 +1379,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0) next_item = self.service_manager_list.topLevelItem(item) self.service_manager_list.setCurrentItem(next_item) - self.live_controller.preview_list_widget.setFocus() + self.live_controller.preview_widget.get_preview_widget().setFocus() else: critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'), translate('OpenLP.ServiceManager', diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 8492b69bb..d1694ca56 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -777,8 +777,8 @@ class SlideController(DisplayController): slideHeight = width * (1 / self.ratio) row += 1 self.slideList[unicode(row)] = row - 1 - self.preview_widget.update_preview_selection(slideno) self.preview_widget.replace_service_manager_item(self.service_item, width, self.ratio, slideno) + self.preview_widget.update_preview_selection(slideno) self.enableToolBar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme From 0fb24d2233f623031f195865db98d87771cf3d62 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 17 Apr 2013 20:33:44 +0200 Subject: [PATCH 157/235] added tests --- openlp/plugins/custom/forms/editcustomform.py | 2 +- .../custom/forms/test_customform.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/custom/forms/editcustomform.py b/openlp/plugins/custom/forms/editcustomform.py index c78baa974..580fbde07 100644 --- a/openlp/plugins/custom/forms/editcustomform.py +++ b/openlp/plugins/custom/forms/editcustomform.py @@ -257,6 +257,6 @@ class EditCustomForm(QtGui.QDialog, Ui_CustomEditDialog): # We must have at least one slide. if self.slide_list_view.count() == 0: critical_error_message_box(message=translate('CustomPlugin.EditCustomForm', - 'You need to add at least one slide')) + 'You need to add at least one slide.')) return False return True diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index 6e6318ca6..dbed244e3 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -62,3 +62,45 @@ class TestEditCustomForm(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.' + + def validate_not_valid_part1_test(self): + """ + Test the _validate() method. + """ + # GIVEN: Mocked methods. + with patch(u'openlp.plugins.custom.forms.editcustomform.critical_error_message_box') as \ + mocked_critical_error_message_box: + mocked_displayText = MagicMock() + mocked_displayText.return_value = u'' + self.form.title_edit.displayText = mocked_displayText + mocked_setFocus = MagicMock() + self.form.title_edit.setFocus = mocked_setFocus + + # WHEN: Call the method. + result = self.form._validate() + + # THEN: The validate method should have returned False. + assert not result, u'The _validate() method should have retured False' + mocked_setFocus.assert_called_with() + mocked_critical_error_message_box.assert_called_with(message=u'You need to type in a title.') + + def validate_not_valid_part2_test(self): + """ + Test the _validate() method. + """ + # GIVEN: Mocked methods. + with patch(u'openlp.plugins.custom.forms.editcustomform.critical_error_message_box') as \ + mocked_critical_error_message_box: + mocked_displayText = MagicMock() + mocked_displayText.return_value = u'something' + self.form.title_edit.displayText = mocked_displayText + mocked_count = MagicMock() + mocked_count.return_value = 0 + self.form.slide_list_view.count = mocked_count + + # WHEN: Call the method. + result = self.form._validate() + + # THEN: The validate method should have returned False. + assert not result, u'The _validate() method should have retured False' + mocked_critical_error_message_box.assert_called_with(message=u'You need to add at least one slide.') From f3b49c4dd50aef21251c7a89efd10f29b7c3041f Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 17 Apr 2013 22:12:53 +0200 Subject: [PATCH 158/235] removed dead code --- openlp/plugins/bibles/forms/bibleimportform.py | 3 --- openlp/plugins/bibles/lib/manager.py | 14 ++------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 847b2b7a2..242dff707 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -94,8 +94,6 @@ class BibleImportForm(OpenLPWizard): button. """ self.selectStack.setCurrentIndex(index) - next_button = self.button(QtGui.QWizard.NextButton) - next_button.setEnabled(BibleFormat.get_availability(index)) def custom_init(self): """ @@ -482,7 +480,6 @@ class BibleImportForm(OpenLPWizard): self.setField(u'proxy_server', settings.value(u'proxy address')) self.setField(u'proxy_username', settings.value(u'proxy username')) self.setField(u'proxy_password', settings.value(u'proxy password')) - self.setField(u'openlp1_location', '') self.setField(u'license_version', self.versionNameEdit.text()) self.setField(u'license_copyright', self.copyrightEdit.text()) self.setField(u'license_permissions', self.permissionsEdit.text()) diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index abcfa7ddc..f701de0aa 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -43,13 +43,11 @@ from osis import OSISBible log = logging.getLogger(__name__) + class BibleFormat(object): """ - This is a special enumeration class that holds the various types of Bibles, - plus a few helper functions to facilitate generic handling of Bible types - for importing. + This is a special enumeration class that holds the various types of Bibles. """ - _format_availability = {} Unknown = -1 OSIS = 0 CSV = 1 @@ -87,14 +85,6 @@ class BibleFormat(object): BibleFormat.WebDownload, ] - @staticmethod - def set_availability(format, available): - BibleFormat._format_availability[format] = available - - @staticmethod - def get_availability(format): - return BibleFormat._format_availability.get(format, True) - class BibleManager(object): """ From 5547035550eebb2e227bdf55e69917be2e5fb7d8 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:17:00 +0200 Subject: [PATCH 159/235] fixed short lines --- openlp/plugins/songs/lib/mediaitem.py | 51 +++++++++++---------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d75124d84..cd1cd72ae 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -76,8 +76,7 @@ class SongMediaItem(MediaManagerItem): self.openLyrics = OpenLyrics(self.plugin.manager) self.single_service_item = False self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) - # Holds information about whether the edit is remotely triggered and - # which Song is required. + # Holds information about whether the edit is remotely triggered and which Song is required. self.remoteSong = -1 self.editItem = None self.quick_preview_allowed = True @@ -89,10 +88,8 @@ class SongMediaItem(MediaManagerItem): dest_file = os.path.join( AppLocation.get_section_data_path(self.plugin.name), u'audio', str(song.id), os.path.split(bga)[1]) check_directory_exists(os.path.split(dest_file)[0]) - shutil.copyfile(os.path.join(AppLocation.get_section_data_path(u'servicemanager'), bga), - dest_file) - song.media_files.append(MediaFile.populate( - weight=i, file_name=dest_file)) + shutil.copyfile(os.path.join(AppLocation.get_section_data_path(u'servicemanager'), bga), dest_file) + song.media_files.append(MediaFile.populate(weight=i, file_name=dest_file)) self.plugin.manager.save_object(song, True) def add_end_header_bar(self): @@ -120,7 +117,7 @@ class SongMediaItem(MediaManagerItem): def config_update(self): """ - IS triggered when the songs config is updated + Is triggered when the songs config is updated """ log.debug(u'config_updated') self.searchAsYouType = Settings().value(self.settings_section + u'/search as type') @@ -211,10 +208,9 @@ class SongMediaItem(MediaManagerItem): of songs """ log.debug(u'on_song_list_load - start') - # Called to redisplay the song list screen edit from a search - # or from the exit of the Song edit dialog. If remote editing is active - # Trigger it and clean up so it will not update again. - # Push edits to the service manager to update items + # Called to redisplay the song list screen edit from a search or from the exit of the Song edit dialog. If + # remote editing is active Trigger it and clean up so it will not update again. Push edits to the service + # manager to update items if self.editItem and self.updateServiceOnEdit and not self.remote_triggered: item = self.build_service_item(self.editItem) self.service_manager.replace_service_item(item) @@ -280,9 +276,8 @@ class SongMediaItem(MediaManagerItem): def on_search_text_edit_changed(self, text): """ - If search as type enabled invoke the search on each key press. - If the Lyrics are being searched do not start till 7 characters - have been entered. + If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start + till 7 characters have been entered. """ if self.searchAsYouType: search_length = 1 @@ -320,9 +315,8 @@ class SongMediaItem(MediaManagerItem): def onRemoteEdit(self, song_id, preview=False): """ - Called by ServiceManager or SlideController by event passing - the Song Id in the payload along with an indicator to say which - type of display is required. + Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator + to say which type of display is required. """ log.debug(u'onRemoteEdit for song %s' % song_id) song_id = int(song_id) @@ -362,11 +356,11 @@ class SongMediaItem(MediaManagerItem): if check_item_selected(self.list_view, UiStrings().SelectDelete): items = self.list_view.selectedIndexes() if QtGui.QMessageBox.question(self, - UiStrings().ConfirmDelete, - translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '', - QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), - QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: + UiStrings().ConfirmDelete, + translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '', + QtCore.QCoreApplication.CodecForTr, len(items)), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), + QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: return self.application.set_busy_cursor() self.main_window.display_progress_bar(len(items)) @@ -430,8 +424,8 @@ class SongMediaItem(MediaManagerItem): verse_tags_translated = True if not song.verse_order.strip(): for verse in verse_list: - # We cannot use from_loose_input() here, because database - # is supposed to contain English lowercase singlechar tags. + # We cannot use from_loose_input() here, because database is supposed to contain English lowercase + # singlechar tags. verse_tag = verse[0][u'type'] verse_index = None if len(verse_tag) > 1: @@ -470,9 +464,7 @@ class SongMediaItem(MediaManagerItem): if Settings().value(u'general/ccli number'): service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value(u'general/ccli number')) - service_item.audit = [ - song.title, author_list, song.copyright, unicode(song.ccli_number) - ] + service_item.audit = [song.title, author_list, song.copyright, unicode(song.ccli_number)] service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)} service_item.xml_version = self.openLyrics.song_to_xml(song) # Add the audio file to the service item. @@ -489,9 +481,8 @@ class SongMediaItem(MediaManagerItem): if self.plugin.status != PluginStatus.Active or not item.data_string: return if item.data_string[u'title'].find(u'@') == -1: - # This file seems to be an old one (prior to 1.9.5), which means, - # that the search title (data_string[u'title']) is probably wrong. - # We add "@" to search title and hope that we do not add any + # FIXME: This file seems to be an old one (prior to 1.9.5), which means, that the search title + # (data_string[u'title']) is probably wrong. We add "@" to search title and hope that we do not add any # duplicate. This should work for songs without alternate title. search_results = self.plugin.manager.get_all_objects(Song, Song.search_title == (re.compile(r'\W+', re.UNICODE).sub(u' ', From cf209fef6f1eac8f4fda593e13dc8f0fcc2d5edb Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:20:56 +0200 Subject: [PATCH 160/235] renaming --- openlp/plugins/songs/lib/mediaitem.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index cd1cd72ae..86b27240b 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -72,7 +72,7 @@ class SongMediaItem(MediaManagerItem): def __init__(self, parent, plugin): self.icon_path = u'songs/song' MediaManagerItem.__init__(self, parent, plugin) - self.editSongForm = EditSongForm(self, self.main_window, self.plugin.manager) + self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager) self.openLyrics = OpenLyrics(self.plugin.manager) self.single_service_item = False self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) @@ -82,7 +82,7 @@ class SongMediaItem(MediaManagerItem): self.quick_preview_allowed = True self.has_search = True - def _updateBackgroundAudio(self, song, item): + def _update_background_audio(self, song, item): song.media_files = [] for i, bga in enumerate(item.background_audio): dest_file = os.path.join( @@ -304,8 +304,8 @@ class SongMediaItem(MediaManagerItem): def on_new_click(self): log.debug(u'on_new_click') - self.editSongForm.new_song() - self.editSongForm.exec_() + self.edit_song_form.new_song() + self.edit_song_form.exec_() self.onClearTextButtonClick() self.on_selection_change() self.auto_select_id = -1 @@ -322,8 +322,8 @@ class SongMediaItem(MediaManagerItem): song_id = int(song_id) valid = self.plugin.manager.get_object(Song, song_id) if valid: - self.editSongForm.load_song(song_id, preview) - if self.editSongForm.exec_() == QtGui.QDialog.Accepted: + self.edit_song_form.load_song(song_id, preview) + if self.edit_song_form.exec_() == QtGui.QDialog.Accepted: self.auto_select_id = -1 self.on_song_list_load() self.remoteSong = song_id @@ -343,8 +343,8 @@ class SongMediaItem(MediaManagerItem): if check_item_selected(self.list_view, UiStrings().SelectEdit): self.editItem = self.list_view.currentItem() item_id = self.editItem.data(QtCore.Qt.UserRole) - self.editSongForm.load_song(item_id, False) - self.editSongForm.exec_() + self.edit_song_form.load_song(item_id, False) + self.edit_song_form.exec_() self.auto_select_id = -1 self.on_song_list_load() self.editItem = None @@ -509,12 +509,12 @@ class SongMediaItem(MediaManagerItem): break # If there's any backing tracks, copy them over. if item.background_audio: - self._updateBackgroundAudio(song, item) + self._update_background_audio(song, item) if add_song and self.addSongFromService: song = self.openLyrics.xml_to_song(item.xml_version) # If there's any backing tracks, copy them over. if item.background_audio: - self._updateBackgroundAudio(song, item) + self._update_background_audio(song, item) editId = song.id self.on_search_text_button_clicked() elif add_song and not self.addSongFromService: @@ -522,7 +522,7 @@ class SongMediaItem(MediaManagerItem): song = self.openLyrics.xml_to_song(item.xml_version, True) # If there's any backing tracks, copy them over. if item.background_audio: - self._updateBackgroundAudio(song, item) + self._update_background_audio(song, item) editId = song.id temporary = True # Update service with correct song id. From c13c0a047495e4d1c61c162353834c2100d094a0 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:22:20 +0200 Subject: [PATCH 161/235] renaming --- openlp/plugins/songs/forms/editsongform.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 30 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 49a20762a..8baadbcc8 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -714,7 +714,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): text = self.song_book_combo_box.currentText() if item == 0 and text: temp_song_book = text - self.media_item.songMaintenanceForm.exec_(True) + self.media_item.song_maintenance_form.exec_(True) self.load_authors() self.load_books() self.load_topics() diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 86b27240b..7900389e2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -75,10 +75,10 @@ class SongMediaItem(MediaManagerItem): self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager) self.openLyrics = OpenLyrics(self.plugin.manager) self.single_service_item = False - self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) + self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self) # Holds information about whether the edit is remotely triggered and which Song is required. - self.remoteSong = -1 - self.editItem = None + self.remote_song = -1 + self.edit_item = None self.quick_preview_allowed = True self.has_search = True @@ -211,8 +211,8 @@ class SongMediaItem(MediaManagerItem): # Called to redisplay the song list screen edit from a search or from the exit of the Song edit dialog. If # remote editing is active Trigger it and clean up so it will not update again. Push edits to the service # manager to update items - if self.editItem and self.updateServiceOnEdit and not self.remote_triggered: - item = self.build_service_item(self.editItem) + if self.edit_item and self.updateServiceOnEdit and not self.remote_triggered: + item = self.build_service_item(self.edit_item) self.service_manager.replace_service_item(item) self.on_search_text_button_clicked() log.debug(u'on_song_list_load - finished') @@ -311,7 +311,7 @@ class SongMediaItem(MediaManagerItem): self.auto_select_id = -1 def onSongMaintenanceClick(self): - self.songMaintenanceForm.exec_() + self.song_maintenance_form.exec_() def onRemoteEdit(self, song_id, preview=False): """ @@ -326,10 +326,10 @@ class SongMediaItem(MediaManagerItem): if self.edit_song_form.exec_() == QtGui.QDialog.Accepted: self.auto_select_id = -1 self.on_song_list_load() - self.remoteSong = song_id + self.remote_song = song_id self.remote_triggered = True item = self.build_service_item(remote=True) - self.remoteSong = -1 + self.remote_song = -1 self.remote_triggered = None if item: return item @@ -341,13 +341,13 @@ class SongMediaItem(MediaManagerItem): """ log.debug(u'on_edit_click') if check_item_selected(self.list_view, UiStrings().SelectEdit): - self.editItem = self.list_view.currentItem() - item_id = self.editItem.data(QtCore.Qt.UserRole) + self.edit_item = self.list_view.currentItem() + item_id = self.edit_item.data(QtCore.Qt.UserRole) self.edit_song_form.load_song(item_id, False) self.edit_song_form.exec_() self.auto_select_id = -1 self.on_song_list_load() - self.editItem = None + self.edit_item = None def on_delete_click(self): """ @@ -390,8 +390,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug(u'onCloneClick') if check_item_selected(self.list_view, UiStrings().SelectEdit): - self.editItem = self.list_view.currentItem() - item_id = self.editItem.data(QtCore.Qt.UserRole) + self.edit_item = self.list_view.currentItem() + item_id = self.edit_item.data(QtCore.Qt.UserRole) old_song = self.plugin.manager.get_object(Song, item_id) song_xml = self.openLyrics.song_to_xml(old_song) new_song = self.openLyrics.xml_to_song(song_xml) @@ -405,8 +405,8 @@ class SongMediaItem(MediaManagerItem): """ Generate the slide data. Needs to be implemented by the plugin. """ - log.debug(u'generate_slide_data: %s, %s, %s' % (service_item, item, self.remoteSong)) - item_id = self._get_id_of_item_to_generate(item, self.remoteSong) + log.debug(u'generate_slide_data: %s, %s, %s' % (service_item, item, self.remote_song)) + item_id = self._get_id_of_item_to_generate(item, self.remote_song) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) From d002cb383ef33c9ef91674ad2ffc68a245d6a3e9 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:27:11 +0200 Subject: [PATCH 162/235] renaming --- openlp/core/lib/mediamanageritem.py | 2 +- openlp/core/ui/mainwindow.py | 2 +- openlp/plugins/bibles/lib/mediaitem.py | 2 +- openlp/plugins/custom/lib/mediaitem.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index f6c95aa9e..0bf34e9c5 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -424,7 +424,7 @@ class MediaManagerItem(QtGui.QWidget): """ raise NotImplementedError(u'MediaManagerItem.on_delete_click needs to be defined by the plugin') - def onFocus(self): + def on_focus(self): """ Run when a tab in the media manager gains focus. This gives the media item a chance to focus any elements it wants to. diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 69439c1b1..91142f0cc 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -560,7 +560,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): """ widget = self.media_tool_box.widget(index) if widget: - widget.onFocus() + widget.on_focus() def version_notice(self, version): """ diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 86a507612..b5352db9f 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -250,7 +250,7 @@ class BibleMediaItem(MediaManagerItem): self.quickSearchEdit.returnPressed.connect(self.onQuickSearchButton) self.searchTabBar.currentChanged.connect(self.onSearchTabBarCurrentChanged) - def onFocus(self): + def on_focus(self): if self.quickTab.isVisible(): self.quickSearchEdit.setFocus() else: diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 071f68839..37d17bd4a 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -178,7 +178,7 @@ class CustomMediaItem(MediaManagerItem): self.plugin.manager.delete_object(CustomSlide, id) self.on_search_text_button_clicked() - def onFocus(self): + def on_focus(self): self.search_text_edit.setFocus() def generate_slide_data(self, service_item, item=None, xmlVersion=False, diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 7900389e2..6e835a025 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -112,7 +112,7 @@ class SongMediaItem(MediaManagerItem): text=translate('OpenLP.MediaManagerItem', '&Clone'), icon=u':/general/general_clone.png', triggers=self.onCloneClick) - def onFocus(self): + def on_focus(self): self.search_text_edit.setFocus() def config_update(self): @@ -120,7 +120,7 @@ class SongMediaItem(MediaManagerItem): Is triggered when the songs config is updated """ log.debug(u'config_updated') - self.searchAsYouType = Settings().value(self.settings_section + u'/search as type') + self.search_as_you_type = Settings().value(self.settings_section + u'/search as type') self.updateServiceOnEdit = Settings().value(self.settings_section + u'/update service on edit') self.addSongFromService = Settings().value(self.settings_section + u'/add song from service',) @@ -279,7 +279,7 @@ class SongMediaItem(MediaManagerItem): If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start till 7 characters have been entered. """ - if self.searchAsYouType: + if self.search_as_you_type: search_length = 1 if self.search_text_edit.current_search_type() == SongSearch.Entire: search_length = 4 From 2d518febf028c8c05170eafed1898703496deb25 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:28:21 +0200 Subject: [PATCH 163/235] renaming --- openlp/plugins/songs/lib/mediaitem.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 6e835a025..416ae6530 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -161,23 +161,23 @@ class SongMediaItem(MediaManagerItem): search_type = self.search_text_edit.current_search_type() if search_type == SongSearch.Entire: log.debug(u'Entire Song Search') - search_results = self.searchEntire(search_keywords) - self.displayResultsSong(search_results) + search_results = self.search_entire(search_keywords) + self.display_results_song(search_results) elif search_type == SongSearch.Titles: log.debug(u'Titles Search') search_results = self.plugin.manager.get_all_objects(Song, Song.search_title.like(u'%' + clean_string(search_keywords) + u'%')) - self.displayResultsSong(search_results) + self.display_results_song(search_results) elif search_type == SongSearch.Lyrics: log.debug(u'Lyrics Search') search_results = self.plugin.manager.get_all_objects(Song, Song.search_lyrics.like(u'%' + clean_string(search_keywords) + u'%')) - self.displayResultsSong(search_results) + self.display_results_song(search_results) elif search_type == SongSearch.Authors: log.debug(u'Authors Search') search_results = self.plugin.manager.get_all_objects(Author, Author.display_name.like(u'%' + search_keywords + u'%'), Author.display_name.asc()) - self.displayResultsAuthor(search_results) + self.display_results_author(search_results) elif search_type == SongSearch.Books: log.debug(u'Books Search') search_results = self.plugin.manager.get_all_objects(Book, @@ -188,15 +188,15 @@ class SongMediaItem(MediaManagerItem): search_results = self.plugin.manager.get_all_objects(Book, Book.name.like(u'%' + search_keywords[0] + u'%'), Book.name.asc()) song_number = re.sub(r'[^0-9]', u'', search_keywords[2]) - self.displayResultsBook(search_results, song_number) + self.display_results_book(search_results, song_number) elif search_type == SongSearch.Themes: log.debug(u'Theme Search') search_results = self.plugin.manager.get_all_objects(Song, Song.theme_name.like(u'%' + search_keywords + u'%')) - self.displayResultsSong(search_results) + self.display_results_song(search_results) self.check_search_result() - def searchEntire(self, search_keywords): + def search_entire(self, search_keywords): return self.plugin.manager.get_all_objects(Song, or_(Song.search_title.like(u'%' + clean_string(search_keywords) + u'%'), Song.search_lyrics.like(u'%' + clean_string(search_keywords) + u'%'), @@ -217,7 +217,7 @@ class SongMediaItem(MediaManagerItem): self.on_search_text_button_clicked() log.debug(u'on_song_list_load - finished') - def displayResultsSong(self, searchresults): + def display_results_song(self, searchresults): log.debug(u'display results Song') self.save_auto_select_id() self.list_view.clear() @@ -237,7 +237,7 @@ class SongMediaItem(MediaManagerItem): self.list_view.setCurrentItem(song_name) self.auto_select_id = -1 - def displayResultsAuthor(self, searchresults): + def display_results_author(self, searchresults): log.debug(u'display results Author') self.list_view.clear() for author in searchresults: @@ -250,7 +250,7 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) - def displayResultsBook(self, searchresults, song_number=False): + def display_results_book(self, searchresults, song_number=False): log.debug(u'display results Book') self.list_view.clear() for book in searchresults: @@ -533,5 +533,5 @@ class SongMediaItem(MediaManagerItem): """ Search for some songs """ - search_results = self.searchEntire(string) + search_results = self.search_entire(string) return [[song.id, song.title] for song in search_results] From 06b8707434b38798c0e0341509fa96b65da75b72 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:30:55 +0200 Subject: [PATCH 164/235] renaming --- openlp/plugins/songs/lib/mediaitem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 416ae6530..d35096210 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -97,7 +97,7 @@ class SongMediaItem(MediaManagerItem): ## Song Maintenance Button ## self.maintenanceAction = self.toolbar.add_toolbar_action('maintenanceAction', icon=':/songs/song_maintenance.png', - triggers=self.onSongMaintenanceClick) + triggers=self.on_song_maintenance_click) self.add_search_to_toolbar() # Signals and slots Registry().register_function(u'songs_load_list', self.on_song_list_load) @@ -110,7 +110,7 @@ class SongMediaItem(MediaManagerItem): create_widget_action(self.list_view, separator=True) create_widget_action(self.list_view, text=translate('OpenLP.MediaManagerItem', '&Clone'), icon=u':/general/general_clone.png', - triggers=self.onCloneClick) + triggers=self.on_clone_click) def on_focus(self): self.search_text_edit.setFocus() @@ -310,7 +310,7 @@ class SongMediaItem(MediaManagerItem): self.on_selection_change() self.auto_select_id = -1 - def onSongMaintenanceClick(self): + def on_song_maintenance_click(self): self.song_maintenance_form.exec_() def onRemoteEdit(self, song_id, preview=False): @@ -384,11 +384,11 @@ class SongMediaItem(MediaManagerItem): self.application.set_normal_cursor() self.on_search_text_button_clicked() - def onCloneClick(self): + def on_clone_click(self): """ Clone a Song """ - log.debug(u'onCloneClick') + log.debug(u'on_clone_click') if check_item_selected(self.list_view, UiStrings().SelectEdit): self.edit_item = self.list_view.currentItem() item_id = self.edit_item.data(QtCore.Qt.UserRole) From 2ad78e4cb9c75e3243d5352a7ed40cf1d9e93c19 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 11:32:48 +0200 Subject: [PATCH 165/235] renaming --- openlp/core/ui/servicemanager.py | 2 +- openlp/core/ui/slidecontroller.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 6e0f4ad95..39e335de6 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1393,7 +1393,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): item = self.find_service_item()[0] if self.service_items[item][u'service_item'].is_capable(ItemCapabilities.CanEdit): new_item = Registry().get(self.service_items[item][u'service_item'].name). \ - onRemoteEdit(self.service_items[item][u'service_item'].edit_id) + on_remote_edit(self.service_items[item][u'service_item'].edit_id) if new_item: self.add_service_item(new_item, replace=True) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index eaeebfba8..811405c40 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1229,7 +1229,7 @@ class SlideController(DisplayController): From the preview display requires the service Item to be editied """ self.song_edit = True - new_item = Registry().get(self.service_item.name).onRemoteEdit(self.service_item.edit_id, True) + new_item = Registry().get(self.service_item.name).on_remote_edit(self.service_item.edit_id, True) if new_item: self.add_service_item(new_item) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d35096210..1f0b10958 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -313,12 +313,12 @@ class SongMediaItem(MediaManagerItem): def on_song_maintenance_click(self): self.song_maintenance_form.exec_() - def onRemoteEdit(self, song_id, preview=False): + def on_remote_edit(self, song_id, preview=False): """ Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator to say which type of display is required. """ - log.debug(u'onRemoteEdit for song %s' % song_id) + log.debug(u'on_remote_edit for song %s' % song_id) song_id = int(song_id) valid = self.plugin.manager.get_object(Song, song_id) if valid: From 14db927af70a23aa5f8f6564887ae17770354f08 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 19:10:26 +0200 Subject: [PATCH 166/235] more renaming --- openlp/plugins/custom/lib/mediaitem.py | 53 ++++++++++++-------------- openlp/plugins/songs/lib/mediaitem.py | 8 ++-- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 37d17bd4a..f51db8e6b 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -63,14 +63,14 @@ class CustomMediaItem(MediaManagerItem): self.has_search = True # Holds information about whether the edit is remotely triggered and # which Custom is required. - self.remoteCustom = -1 + self.remote_custom = -1 self.manager = plugin.manager def add_end_header_bar(self): self.toolbar.addSeparator() self.add_search_to_toolbar() # Signals and slots - QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'cleared()'), self.onClearTextButtonClick) + QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'cleared()'), self.on_clear_text_button_click) QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'searchTypeChanged(int)'), self.on_search_text_button_clicked) Registry().register_function(u'custom_load_list', self.load_list) @@ -119,14 +119,13 @@ class CustomMediaItem(MediaManagerItem): def on_new_click(self): self.edit_custom_form.load_custom(0) self.edit_custom_form.exec_() - self.onClearTextButtonClick() + self.on_clear_text_button_click() self.on_selection_change() - def onRemoteEdit(self, custom_id, preview=False): + def on_remote_edit(self, custom_id, preview=False): """ - Called by ServiceManager or SlideController by event passing - the custom Id in the payload along with an indicator to say which - type of display is required. + Called by ServiceManager or SlideController by event passing the custom Id in the payload along with an + indicator to say which type of display is required. """ custom_id = int(custom_id) valid = self.manager.get_object(CustomSlide, custom_id) @@ -134,12 +133,12 @@ class CustomMediaItem(MediaManagerItem): self.edit_custom_form.load_custom(custom_id, preview) if self.edit_custom_form.exec_() == QtGui.QDialog.Accepted: self.remote_triggered = True - self.remoteCustom = custom_id + self.remote_custom = custom_id self.auto_select_id = -1 self.on_search_text_button_clicked() item = self.build_service_item(remote=True) self.remote_triggered = None - self.remoteCustom = 1 + self.remote_custom = 1 if item: return item return None @@ -163,17 +162,16 @@ class CustomMediaItem(MediaManagerItem): if check_item_selected(self.list_view, UiStrings().SelectDelete): items = self.list_view.selectedIndexes() if QtGui.QMessageBox.question(self, - UiStrings().ConfirmDelete, - translate('CustomPlugin.MediaItem', - 'Are you sure you want to delete the %n selected custom slide(s)?', '', - QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), - QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: + UiStrings().ConfirmDelete, + translate('CustomPlugin.MediaItem', + 'Are you sure you want to delete the %n selected custom slide(s)?', '', + QtCore.QCoreApplication.CodecForTr, len(items)), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), + QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: return row_list = [item.row() for item in self.list_view.selectedIndexes()] row_list.sort(reverse=True) - id_list = [(item.data(QtCore.Qt.UserRole)) - for item in self.list_view.selectedIndexes()] + id_list = [(item.data(QtCore.Qt.UserRole)) for item in self.list_view.selectedIndexes()] for id in id_list: self.plugin.manager.delete_object(CustomSlide, id) self.on_search_text_button_clicked() @@ -186,20 +184,20 @@ class CustomMediaItem(MediaManagerItem): """ Generate the slide data. Needs to be implemented by the plugin. """ - item_id = self._get_id_of_item_to_generate(item, self.remoteCustom) + item_id = self._get_id_of_item_to_generate(item, self.remote_custom) service_item.add_capability(ItemCapabilities.CanEdit) service_item.add_capability(ItemCapabilities.CanPreview) service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.CanSoftBreak) service_item.add_capability(ItemCapabilities.OnLoadUpdate) - customSlide = self.plugin.manager.get_object(CustomSlide, item_id) - title = customSlide.title - credit = customSlide.credits + custom_slide = self.plugin.manager.get_object(CustomSlide, item_id) + title = custom_slide.title + credit = custom_slide.credits service_item.edit_id = item_id - theme = customSlide.theme_name + theme = custom_slide.theme_name if theme: service_item.theme = theme - custom_xml = CustomXMLParser(customSlide.text) + custom_xml = CustomXMLParser(custom_slide.text) verse_list = custom_xml.get_verses() raw_slides = [verse[1] for verse in verse_list] service_item.title = title @@ -234,15 +232,14 @@ class CustomMediaItem(MediaManagerItem): def on_search_text_edit_changed(self, text): """ - If search as type enabled invoke the search on each key press. - If the Title is being searched do not start until 2 characters - have been entered. + If search as type enabled invoke the search on each key press. If the Title is being searched do not start until + 2 characters have been entered. """ search_length = 2 if len(text) > search_length: self.on_search_text_button_clicked() elif not text: - self.onClearTextButtonClick() + self.on_clear_text_button_click() def service_load(self, item): """ @@ -287,7 +284,7 @@ class CustomMediaItem(MediaManagerItem): if item.name.lower() == u'custom': Registry().execute(u'service_item_update', u'%s:%s:%s' % (custom.id, item.unique_identifier, False)) - def onClearTextButtonClick(self): + def on_clear_text_button_click(self): """ Clear the search text. """ diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 1f0b10958..c0626ebba 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -102,7 +102,7 @@ class SongMediaItem(MediaManagerItem): # Signals and slots Registry().register_function(u'songs_load_list', self.on_song_list_load) Registry().register_function(u'songs_preview', self.on_preview_click) - QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'cleared()'), self.onClearTextButtonClick) + QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'cleared()'), self.on_clear_text_button_click) QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL(u'searchTypeChanged(int)'), self.on_search_text_button_clicked) @@ -267,7 +267,7 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) - def onClearTextButtonClick(self): + def on_clear_text_button_click(self): """ Clear the search text. """ @@ -288,7 +288,7 @@ class SongMediaItem(MediaManagerItem): if len(text) > search_length: self.on_search_text_button_clicked() elif not text: - self.onClearTextButtonClick() + self.on_clear_text_button_click() def on_import_click(self): if not hasattr(self, u'import_wizard'): @@ -306,7 +306,7 @@ class SongMediaItem(MediaManagerItem): log.debug(u'on_new_click') self.edit_song_form.new_song() self.edit_song_form.exec_() - self.onClearTextButtonClick() + self.on_clear_text_button_click() self.on_selection_change() self.auto_select_id = -1 From 85ab27ab0c3bbc6484c8862f74cbc9a3c779f698 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 19:17:00 +0200 Subject: [PATCH 167/235] alters plugin renaming --- openlp/plugins/alerts/__init__.py | 3 +-- openlp/plugins/alerts/alertsplugin.py | 20 ++++++++--------- openlp/plugins/alerts/forms/__init__.py | 25 +++++++++------------- openlp/plugins/alerts/forms/alertdialog.py | 2 +- openlp/plugins/alerts/forms/alertform.py | 13 ++++++----- openlp/plugins/alerts/lib/alertsmanager.py | 14 ++++++------ openlp/plugins/alerts/lib/db.py | 5 +++-- 7 files changed, 36 insertions(+), 46 deletions(-) diff --git a/openlp/plugins/alerts/__init__.py b/openlp/plugins/alerts/__init__.py index 3e4a63444..bb4c2835d 100644 --- a/openlp/plugins/alerts/__init__.py +++ b/openlp/plugins/alerts/__init__.py @@ -27,6 +27,5 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`alerts` module provides the Alerts plugin for producing impromptu -on-screen announcements during a service. +The :mod:`alerts` module provides the Alerts plugin for producing impromptu on-screen announcements during a service. """ diff --git a/openlp/plugins/alerts/alertsplugin.py b/openlp/plugins/alerts/alertsplugin.py index 8d8530918..a2d77c42b 100644 --- a/openlp/plugins/alerts/alertsplugin.py +++ b/openlp/plugins/alerts/alertsplugin.py @@ -115,13 +115,13 @@ HTML = """ """ __default_settings__ = { - u'alerts/font face': QtGui.QFont().family(), - u'alerts/font size': 40, - u'alerts/db type': u'sqlite', - u'alerts/location': AlertLocation.Bottom, - u'alerts/background color': u'#660000', - u'alerts/font color': u'#ffffff', - u'alerts/timeout': 5 + u'alerts/font face': QtGui.QFont().family(), + u'alerts/font size': 40, + u'alerts/db type': u'sqlite', + u'alerts/location': AlertLocation.Bottom, + u'alerts/background color': u'#660000', + u'alerts/font color': u'#ffffff', + u'alerts/timeout': 5 } @@ -139,12 +139,10 @@ class AlertsPlugin(Plugin): def add_tools_menu_item(self, tools_menu): """ - Give the alerts plugin the opportunity to add items to the - **Tools** menu. + Give the alerts 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.tools_alert_item = create_action(tools_menu, u'toolsAlertItem', diff --git a/openlp/plugins/alerts/forms/__init__.py b/openlp/plugins/alerts/forms/__init__.py index e97fdfed3..2a3037f97 100644 --- a/openlp/plugins/alerts/forms/__init__.py +++ b/openlp/plugins/alerts/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 alertform import AlertForm diff --git a/openlp/plugins/alerts/forms/alertdialog.py b/openlp/plugins/alerts/forms/alertdialog.py index 1985fdcd1..e8a50d183 100644 --- a/openlp/plugins/alerts/forms/alertdialog.py +++ b/openlp/plugins/alerts/forms/alertdialog.py @@ -71,7 +71,7 @@ class Ui_AlertDialog(object): self.save_button.setObjectName(u'save_button') self.manage_button_layout.addWidget(self.save_button) self.delete_button = create_button(alert_dialog, u'delete_button', role=u'delete', enabled=False, - click=alert_dialog.onDeleteButtonClicked) + click=alert_dialog.on_delete_button_clicked) self.manage_button_layout.addWidget(self.delete_button) self.manage_button_layout.addStretch() self.alert_dialog_layout.addLayout(self.manage_button_layout, 1, 1) diff --git a/openlp/plugins/alerts/forms/alertform.py b/openlp/plugins/alerts/forms/alertform.py index 64aca1e26..d1f5361d0 100644 --- a/openlp/plugins/alerts/forms/alertform.py +++ b/openlp/plugins/alerts/forms/alertform.py @@ -93,7 +93,7 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): if self.trigger_alert(self.alert_text_edit.text()): self.close() - def onDeleteButtonClicked(self): + def on_delete_button_clicked(self): """ Deletes the selected item. """ @@ -160,8 +160,7 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): def on_single_click(self): """ - List item has been single clicked to add it to the edit field so it can - be changed. + List item has been single clicked to add it to the edit field so it can be changed. """ item = self.alert_list_widget.selectedIndexes()[0] bitem = self.alert_list_widget.item(item.row()) @@ -186,7 +185,7 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): translate('AlertsPlugin.AlertForm', 'No Parameter Found'), translate('AlertsPlugin.AlertForm', 'You have not entered a parameter to be replaced.\n' 'Do you want to continue anyway?'), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: self.parameter_edit.setFocus() return False # The ParameterEdit field is not empty, but we have not found '<>' @@ -195,7 +194,7 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): translate('AlertsPlugin.AlertForm', 'No Placeholder Found'), translate('AlertsPlugin.AlertForm', 'The alert text does not contain \'<>\'.\n' 'Do you want to continue anyway?'), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: self.parameter_edit.setFocus() return False text = text.replace(u'<>', self.parameter_edit.text()) @@ -204,8 +203,8 @@ class AlertForm(QtGui.QDialog, Ui_AlertDialog): def on_current_row_changed(self, row): """ - Called when the *alert_list_widget*'s current row has been changed. This - enables or disables buttons which require an item to act on. + Called when the *alert_list_widget*'s current row has been changed. This enables or disables buttons which + require an item to act on. ``row`` The row (int). If there is no current row, the value is -1. diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 042999a11..607706a13 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -27,8 +27,8 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`~openlp.plugins.alerts.lib.alertsmanager` module contains the part of -the plugin which manages storing and displaying of alerts. +The :mod:`~openlp.plugins.alerts.lib.alertsmanager` module contains the part of the plugin which manages storing and +displaying of alerts. """ import logging @@ -56,15 +56,14 @@ class AlertsManager(QtCore.QObject): def alert_text(self, message): """ - Called via a alerts_text event. Message is single element array - containing text + Called via a alerts_text event. Message is single element array containing text. """ if message: self.display_alert(message[0]) def display_alert(self, text=u''): """ - Called from the Alert Tab to display an alert + Called from the Alert Tab to display an alert. ``text`` display text @@ -81,7 +80,7 @@ class AlertsManager(QtCore.QObject): def generate_alert(self): """ - Format and request the Alert and start the timer + Format and request the Alert and start the timer. """ log.debug(u'Generate Alert called') if not self.alert_list: @@ -95,8 +94,7 @@ class AlertsManager(QtCore.QObject): def timerEvent(self, event): """ - Time has finished so if our time then request the next Alert - if there is one and reset the timer. + Time has finished so if our time then request the next Alert if there is one and reset the timer. ``event`` the QT event that has been triggered. diff --git a/openlp/plugins/alerts/lib/db.py b/openlp/plugins/alerts/lib/db.py index 58ee47076..499e63906 100644 --- a/openlp/plugins/alerts/lib/db.py +++ b/openlp/plugins/alerts/lib/db.py @@ -27,8 +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 Alerts plugin +The :mod:`db` module provides the database and schema that is the backend for the Alerts plugin. """ from sqlalchemy import Column, Table, types @@ -36,12 +35,14 @@ from sqlalchemy.orm import mapper from openlp.core.lib.db import BaseModel, init_db + class AlertItem(BaseModel): """ AlertItem model """ pass + def init_schema(url): """ Setup the alerts database connection and initialise the database schema From 098d733ad1ace0fd10ba97d51894f0b45d9aa95f Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 18 Apr 2013 19:45:14 +0200 Subject: [PATCH 168/235] bible renamings --- openlp/core/ui/firsttimelanguagedialog.py | 52 ++--- openlp/core/ui/firsttimelanguageform.py | 8 +- openlp/plugins/bibles/__init__.py | 3 +- openlp/plugins/bibles/bibleplugin.py | 63 +++--- openlp/plugins/bibles/forms/__init__.py | 30 ++- .../plugins/bibles/forms/bibleimportform.py | 10 +- .../plugins/bibles/forms/bibleupgradeform.py | 94 ++++---- openlp/plugins/bibles/forms/booknamedialog.py | 110 +++++----- openlp/plugins/bibles/forms/booknameform.py | 62 +++--- .../plugins/bibles/forms/editbibledialog.py | 200 +++++++++--------- openlp/plugins/bibles/forms/editbibleform.py | 48 ++--- openlp/plugins/bibles/forms/languagedialog.py | 68 +++--- openlp/plugins/bibles/forms/languageform.py | 16 +- openlp/plugins/bibles/lib/__init__.py | 61 +++--- openlp/plugins/bibles/lib/csvbible.py | 13 +- openlp/plugins/bibles/lib/db.py | 2 +- openlp/plugins/bibles/lib/http.py | 3 +- openlp/plugins/bibles/lib/mediaitem.py | 80 +++---- openlp/plugins/bibles/lib/opensong.py | 20 +- openlp/plugins/bibles/lib/osis.py | 14 +- openlp/plugins/bibles/lib/upgrade.py | 3 +- .../plugins/bibles/lib/versereferencelist.py | 4 +- 22 files changed, 465 insertions(+), 499 deletions(-) diff --git a/openlp/core/ui/firsttimelanguagedialog.py b/openlp/core/ui/firsttimelanguagedialog.py index 21f4dabcd..55fce4399 100644 --- a/openlp/core/ui/firsttimelanguagedialog.py +++ b/openlp/core/ui/firsttimelanguagedialog.py @@ -39,39 +39,39 @@ class Ui_FirstTimeLanguageDialog(object): """ The UI widgets of the language selection dialog. """ - def setupUi(self, languageDialog): + def setupUi(self, language_dialog): """ Set up the UI. """ - languageDialog.setObjectName(u'languageDialog') - languageDialog.resize(300, 50) - self.dialogLayout = QtGui.QVBoxLayout(languageDialog) - self.dialogLayout.setContentsMargins(8, 8, 8, 8) - self.dialogLayout.setSpacing(8) - self.dialogLayout.setObjectName(u'dialog_layout') - self.infoLabel = QtGui.QLabel(languageDialog) - self.infoLabel.setObjectName(u'infoLabel') - self.dialogLayout.addWidget(self.infoLabel) - self.languageLayout = QtGui.QHBoxLayout() - self.languageLayout.setObjectName(u'languageLayout') - self.languageLabel = QtGui.QLabel(languageDialog) - self.languageLabel.setObjectName(u'languageLabel') - self.languageLayout.addWidget(self.languageLabel) - self.languageComboBox = QtGui.QComboBox(languageDialog) - self.languageComboBox.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) - self.languageComboBox.setObjectName("languageComboBox") - self.languageLayout.addWidget(self.languageComboBox) - self.dialogLayout.addLayout(self.languageLayout) - self.button_box = create_button_box(languageDialog, u'button_box', [u'cancel', u'ok']) - self.dialogLayout.addWidget(self.button_box) - self.retranslateUi(languageDialog) + language_dialog.setObjectName(u'language_dialog') + language_dialog.resize(300, 50) + self.dialog_layout = QtGui.QVBoxLayout(language_dialog) + self.dialog_layout.setContentsMargins(8, 8, 8, 8) + self.dialog_layout.setSpacing(8) + self.dialog_layout.setObjectName(u'dialog_layout') + self.info_label = QtGui.QLabel(language_dialog) + self.info_label.setObjectName(u'info_label') + self.dialog_layout.addWidget(self.info_label) + self.language_layout = QtGui.QHBoxLayout() + self.language_layout.setObjectName(u'language_layout') + self.language_label = QtGui.QLabel(language_dialog) + self.language_label.setObjectName(u'language_label') + self.language_layout.addWidget(self.language_label) + self.language_combo_box = QtGui.QComboBox(language_dialog) + self.language_combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.language_combo_box.setObjectName("language_combo_box") + self.language_layout.addWidget(self.language_combo_box) + self.dialog_layout.addLayout(self.language_layout) + self.button_box = create_button_box(language_dialog, u'button_box', [u'cancel', u'ok']) + self.dialog_layout.addWidget(self.button_box) + self.retranslateUi(language_dialog) self.setMaximumHeight(self.sizeHint().height()) - def retranslateUi(self, languageDialog): + def retranslateUi(self, language_dialog): """ Translate the UI on the fly. """ self.setWindowTitle(translate('OpenLP.FirstTimeLanguageForm', 'Select Translation')) - self.infoLabel.setText( + self.info_label.setText( translate('OpenLP.FirstTimeLanguageForm', 'Choose the translation you\'d like to use in OpenLP.')) - self.languageLabel.setText(translate('OpenLP.FirstTimeLanguageForm', 'Translation:')) + self.language_label.setText(translate('OpenLP.FirstTimeLanguageForm', 'Translation:')) diff --git a/openlp/core/ui/firsttimelanguageform.py b/openlp/core/ui/firsttimelanguageform.py index 85e98e114..14b9dadc3 100644 --- a/openlp/core/ui/firsttimelanguageform.py +++ b/openlp/core/ui/firsttimelanguageform.py @@ -47,8 +47,8 @@ class FirstTimeLanguageForm(QtGui.QDialog, Ui_FirstTimeLanguageDialog): QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.qmList = LanguageManager.get_qm_list() - self.languageComboBox.addItem(u'Autodetect') - self.languageComboBox.addItems(sorted(self.qmList.keys())) + self.language_combo_box.addItem(u'Autodetect') + self.language_combo_box.addItems(sorted(self.qmList.keys())) def exec_(self): """ @@ -61,12 +61,12 @@ class FirstTimeLanguageForm(QtGui.QDialog, Ui_FirstTimeLanguageDialog): Run when the dialog is OKed. """ # It's the first row so must be Automatic - if self.languageComboBox.currentIndex() == 0: + if self.language_combo_box.currentIndex() == 0: LanguageManager.auto_language = True LanguageManager.set_language(False, False) else: LanguageManager.auto_language = False - action = create_action(None, self.languageComboBox.currentText()) + action = create_action(None, self.language_combo_box.currentText()) LanguageManager.set_language(action, False) return QtGui.QDialog.accept(self) diff --git a/openlp/plugins/bibles/__init__.py b/openlp/plugins/bibles/__init__.py index 1c1510e97..62e6c5f90 100644 --- a/openlp/plugins/bibles/__init__.py +++ b/openlp/plugins/bibles/__init__.py @@ -27,6 +27,5 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`bibles` module provides the Bible plugin to enable OpenLP to display -scripture. +The :mod:`bibles` module provides the Bible plugin to enable OpenLP to display scripture. """ diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index 1f17ae39a..e2888d67a 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -43,25 +43,25 @@ log = logging.getLogger(__name__) __default_settings__ = { - u'bibles/db type': u'sqlite', - u'bibles/last search type': BibleSearch.Reference, - u'bibles/verse layout style': LayoutStyle.VersePerSlide, - u'bibles/book name language': LanguageSelection.Bible, - u'bibles/display brackets': DisplayStyle.NoBrackets, - u'bibles/display new chapter': False, - u'bibles/second bibles': True, - u'bibles/advanced bible': u'', - u'bibles/quick bible': u'', - u'bibles/proxy name': u'', - u'bibles/proxy address': u'', - u'bibles/proxy username': u'', - u'bibles/proxy password': u'', - u'bibles/bible theme': u'', - u'bibles/verse separator': u'', - u'bibles/range separator': u'', - u'bibles/list separator': u'', - u'bibles/end separator': u'', - u'bibles/last directory import': u'' + u'bibles/db type': u'sqlite', + u'bibles/last search type': BibleSearch.Reference, + u'bibles/verse layout style': LayoutStyle.VersePerSlide, + u'bibles/book name language': LanguageSelection.Bible, + u'bibles/display brackets': DisplayStyle.NoBrackets, + u'bibles/display new chapter': False, + u'bibles/second bibles': True, + u'bibles/advanced bible': u'', + u'bibles/quick bible': u'', + u'bibles/proxy name': u'', + u'bibles/proxy address': u'', + u'bibles/proxy username': u'', + u'bibles/proxy password': u'', + u'bibles/bible theme': u'', + u'bibles/verse separator': u'', + u'bibles/range separator': u'', + u'bibles/list separator': u'', + u'bibles/end separator': u'', + u'bibles/last directory import': u'' } @@ -124,18 +124,15 @@ class BiblePlugin(Plugin): def add_export_menu_Item(self, export_menu): self.export_bible_item = create_action(export_menu, u'exportBibleItem', - text=translate('BiblesPlugin', '&Bible'), - visible=False) + text=translate('BiblesPlugin', '&Bible'), visible=False) export_menu.addAction(self.export_bible_item) def add_tools_menu_item(self, tools_menu): """ - Give the bible plugin the opportunity to add items to the - **Tools** menu. + Give the bible 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.debug(u'add tools menu') self.tools_upgrade_item = create_action(tools_menu, u'toolsUpgradeItem', @@ -166,25 +163,23 @@ class BiblePlugin(Plugin): def uses_theme(self, theme): """ - Called to find out if the bible plugin is currently using a theme. - Returns ``True`` if the theme is being used, otherwise returns - ``False``. + Called to find out if the bible plugin is currently using a theme. Returns ``True`` if the theme is being used, + otherwise returns ``False``. """ return unicode(self.settings_tab.bible_theme) == theme - def rename_theme(self, oldTheme, newTheme): + def rename_theme(self, old_theme, new_theme): """ Rename the theme the bible plugin is using making the plugin use the new name. - ``oldTheme`` - The name of the theme the plugin should stop using. Unused for - this particular plugin. + ``old_theme`` + The name of the theme the plugin should stop using. Unused for this particular plugin. - ``newTheme`` + ``new_theme`` The new name the plugin should now use. """ - self.settings_tab.bible_theme = newTheme + self.settings_tab.bible_theme = new_theme self.settings_tab.save() def set_plugin_text_strings(self): diff --git a/openlp/plugins/bibles/forms/__init__.py b/openlp/plugins/bibles/forms/__init__.py index ee0922695..abef5c85c 100644 --- a/openlp/plugins/bibles/forms/__init__.py +++ b/openlp/plugins/bibles/forms/__init__.py @@ -28,30 +28,25 @@ ############################################################################### """ -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 BibleImportForm(QtGui.QWizard, Ui_BibleImportWizard): - def __init__(self, parent, manager, bibleplugin): + def __init__(self, parent, manager, bible_plugin): QtGui.QWizard.__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 booknameform import BookNameForm from languageform import LanguageForm @@ -59,5 +54,4 @@ from bibleimportform import BibleImportForm from bibleupgradeform import BibleUpgradeForm from editbibleform import EditBibleForm -__all__ = [u'BookNameForm', u'LanguageForm', u'BibleImportForm', - u'BibleUpgradeForm', u'EditBibleForm'] +__all__ = [u'BookNameForm', u'LanguageForm', u'BibleImportForm', u'BibleUpgradeForm', u'EditBibleForm'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index f8d771e77..7cdd81229 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -58,12 +58,12 @@ class WebDownload(object): class BibleImportForm(OpenLPWizard): """ - This is the Bible Import Wizard, which allows easy importing of Bibles - into OpenLP from other formats like OSIS, CSV and OpenSong. + This is the Bible Import Wizard, which allows easy importing of Bibles into OpenLP from other formats like OSIS, + CSV and OpenSong. """ log.info(u'BibleImportForm loaded') - def __init__(self, parent, manager, bibleplugin): + def __init__(self, parent, manager, bible_plugin): """ Instantiate the wizard, and run any extra setup we need to. @@ -73,12 +73,12 @@ class BibleImportForm(OpenLPWizard): ``manager`` The Bible manager. - ``bibleplugin`` + ``bible_plugin`` The Bible plugin. """ self.manager = manager self.web_bible_list = {} - OpenLPWizard.__init__(self, parent, bibleplugin, u'bibleImportWizard', u':/wizards/wizard_importbible.bmp') + OpenLPWizard.__init__(self, parent, bible_plugin, u'bibleImportWizard', u':/wizards/wizard_importbible.bmp') def setupUi(self, image): """ diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index 0127d19be..d8f329ee7 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -48,8 +48,8 @@ log = logging.getLogger(__name__) class BibleUpgradeForm(OpenLPWizard): """ - This is the Bible Upgrade Wizard, which allows easy importing of Bibles - into OpenLP from older OpenLP2 database versions. + This is the Bible Upgrade Wizard, which allows easy importing of Bibles into OpenLP from older OpenLP2 database + versions. """ log.info(u'BibleUpgradeForm loaded') @@ -63,7 +63,7 @@ class BibleUpgradeForm(OpenLPWizard): ``manager`` The Bible manager. - ``bibleplugin`` + ``bible_plugin`` The Bible plugin. """ self.manager = manager @@ -74,7 +74,7 @@ class BibleUpgradeForm(OpenLPWizard): self.temp_dir = os.path.join(unicode(gettempdir(), get_filesystem_encoding()), u'openlp') self.files = self.manager.old_bible_databases self.success = {} - self.newbibles = {} + self.new_bibles = {} OpenLPWizard.__init__(self, parent, bible_plugin, u'bibleUpgradeWizard', u':/wizards/wizard_importbible.bmp') def setupUi(self, image): @@ -159,41 +159,41 @@ class BibleUpgradeForm(OpenLPWizard): Add the bible import specific wizard pages. """ # Backup Page - self.backupPage = QtGui.QWizardPage() - self.backupPage.setObjectName(u'BackupPage') - self.backupLayout = QtGui.QVBoxLayout(self.backupPage) + self.backup_page = QtGui.QWizardPage() + self.backup_page.setObjectName(u'BackupPage') + self.backupLayout = QtGui.QVBoxLayout(self.backup_page) self.backupLayout.setObjectName(u'BackupLayout') - self.backupInfoLabel = QtGui.QLabel(self.backupPage) + self.backupInfoLabel = QtGui.QLabel(self.backup_page) self.backupInfoLabel.setOpenExternalLinks(True) self.backupInfoLabel.setTextFormat(QtCore.Qt.RichText) self.backupInfoLabel.setWordWrap(True) self.backupInfoLabel.setObjectName(u'backupInfoLabel') self.backupLayout.addWidget(self.backupInfoLabel) - self.selectLabel = QtGui.QLabel(self.backupPage) + self.selectLabel = QtGui.QLabel(self.backup_page) self.selectLabel.setObjectName(u'select_label') self.backupLayout.addWidget(self.selectLabel) self.formLayout = QtGui.QFormLayout() self.formLayout.setMargin(0) self.formLayout.setObjectName(u'FormLayout') - self.backupDirectoryLabel = QtGui.QLabel(self.backupPage) + self.backupDirectoryLabel = QtGui.QLabel(self.backup_page) self.backupDirectoryLabel.setObjectName(u'backupDirectoryLabel') self.backupDirectoryLayout = QtGui.QHBoxLayout() self.backupDirectoryLayout.setObjectName(u'BackupDirectoryLayout') - self.backupDirectoryEdit = QtGui.QLineEdit(self.backupPage) + self.backupDirectoryEdit = QtGui.QLineEdit(self.backup_page) self.backupDirectoryEdit.setObjectName(u'BackupFolderEdit') self.backupDirectoryLayout.addWidget(self.backupDirectoryEdit) - self.backupBrowseButton = QtGui.QToolButton(self.backupPage) + self.backupBrowseButton = QtGui.QToolButton(self.backup_page) self.backupBrowseButton.setIcon(self.open_icon) self.backupBrowseButton.setObjectName(u'BackupBrowseButton') self.backupDirectoryLayout.addWidget(self.backupBrowseButton) self.formLayout.addRow(self.backupDirectoryLabel, self.backupDirectoryLayout) self.backupLayout.addLayout(self.formLayout) - self.noBackupCheckBox = QtGui.QCheckBox(self.backupPage) + self.noBackupCheckBox = QtGui.QCheckBox(self.backup_page) self.noBackupCheckBox.setObjectName('NoBackupCheckBox') self.backupLayout.addWidget(self.noBackupCheckBox) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) self.backupLayout.addItem(self.spacer) - self.addPage(self.backupPage) + self.addPage(self.backup_page) # Select Page self.selectPage = QtGui.QWizardPage() self.selectPage.setObjectName(u'SelectPage') @@ -247,8 +247,8 @@ class BibleUpgradeForm(OpenLPWizard): self.information_label.setText(translate('BiblesPlugin.UpgradeWizardForm', 'This wizard will help you to upgrade your existing Bibles from a prior version of OpenLP 2. ' 'Click the next button below to start the upgrade process.')) - self.backupPage.setTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Select Backup Directory')) - self.backupPage.setSubTitle(translate('BiblesPlugin.UpgradeWizardForm', + self.backup_page.setTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Select Backup Directory')) + self.backup_page.setSubTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Please select a backup directory for your Bibles')) self.backupInfoLabel.setText(translate('BiblesPlugin.UpgradeWizardForm', 'Previous releases of OpenLP 2.0 are unable to use upgraded Bibles.' @@ -277,7 +277,7 @@ class BibleUpgradeForm(OpenLPWizard): """ if self.currentPage() == self.welcome_page: return True - elif self.currentPage() == self.backupPage: + elif self.currentPage() == self.backup_page: if not self.noBackupCheckBox.checkState() == QtCore.Qt.Checked: backup_path = self.backupDirectoryEdit.text() if not backup_path: @@ -316,7 +316,7 @@ class BibleUpgradeForm(OpenLPWizard): settings.beginGroup(self.plugin.settings_section) self.stop_import_flag = False self.success.clear() - self.newbibles.clear() + self.new_bibles.clear() self.clearScrollArea() self.files = self.manager.old_bible_databases self.addScrollArea() @@ -372,8 +372,8 @@ class BibleUpgradeForm(OpenLPWizard): name = filename[1] self.progress_label.setText(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nUpgrading ...') % (number + 1, max_bibles, name)) - self.newbibles[number] = BibleDB(self.media_item, path=self.path, name=name, file=filename[0]) - self.newbibles[number].register(self.plugin.upgrade_wizard) + self.new_bibles[number] = BibleDB(self.media_item, path=self.path, name=name, file=filename[0]) + self.new_bibles[number].register(self.plugin.upgrade_wizard) metadata = old_bible.get_metadata() web_bible = False meta_data = {} @@ -387,7 +387,7 @@ class BibleUpgradeForm(OpenLPWizard): # Copy the metadata meta_data[meta[u'key']] = meta[u'value'] if meta[u'key'] != u'name' and meta[u'key'] != u'dbversion': - self.newbibles[number].save_meta(meta[u'key'], meta[u'value']) + self.new_bibles[number].save_meta(meta[u'key'], meta[u'value']) if meta[u'key'] == u'download_source': web_bible = True self.includeWebBible = True @@ -403,8 +403,8 @@ class BibleUpgradeForm(OpenLPWizard): if not books: log.error(u'Upgrading books from %s - download name: "%s" failed' % ( meta_data[u'download_source'], meta_data[u'download_name'])) - self.newbibles[number].session.close() - del self.newbibles[number] + self.new_bibles[number].session.close() + del self.new_bibles[number] critical_error_message_box( translate('BiblesPlugin.UpgradeWizardForm', 'Download Error'), translate('BiblesPlugin.UpgradeWizardForm', @@ -419,14 +419,14 @@ class BibleUpgradeForm(OpenLPWizard): meta_data[u'download_source'].lower()) if bible and bible[u'language_id']: language_id = bible[u'language_id'] - self.newbibles[number].save_meta(u'language_id', + self.new_bibles[number].save_meta(u'language_id', language_id) else: - language_id = self.newbibles[number].get_language(name) + language_id = self.new_bibles[number].get_language(name) if not language_id: log.warn(u'Upgrading from "%s" failed' % filename[0]) - self.newbibles[number].session.close() - del self.newbibles[number] + self.new_bibles[number].session.close() + del self.new_bibles[number] self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nFailed') % (number + 1, max_bibles, name), self.progress_bar.maximum() - self.progress_bar.value()) @@ -439,17 +439,17 @@ class BibleUpgradeForm(OpenLPWizard): break self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nUpgrading %s ...') % (number + 1, max_bibles, name, book)) - book_ref_id = self.newbibles[number].\ + book_ref_id = self.new_bibles[number].\ get_book_ref_id_by_name(book, len(books), language_id) if not book_ref_id: log.warn(u'Upgrading books from %s - download name: "%s" aborted by user' % ( meta_data[u'download_source'], meta_data[u'download_name'])) - self.newbibles[number].session.close() - del self.newbibles[number] + self.new_bibles[number].session.close() + del self.new_bibles[number] self.success[number] = False break book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - db_book = self.newbibles[number].create_book(book, + db_book = self.new_bibles[number].create_book(book, book_ref_id, book_details[u'testament_id']) # Try to import already downloaded verses. oldbook = old_bible.get_book(book) @@ -462,19 +462,19 @@ class BibleUpgradeForm(OpenLPWizard): if self.stop_import_flag: self.success[number] = False break - self.newbibles[number].create_verse(db_book.id, + self.new_bibles[number].create_verse(db_book.id, int(verse[u'chapter']), int(verse[u'verse']), unicode(verse[u'text'])) self.application.process_events() - self.newbibles[number].session.commit() + self.new_bibles[number].session.commit() else: - language_id = self.newbibles[number].get_object(BibleMeta, u'language_id') + language_id = self.new_bibles[number].get_object(BibleMeta, u'language_id') if not language_id: - language_id = self.newbibles[number].get_language(name) + language_id = self.new_bibles[number].get_language(name) if not language_id: log.warn(u'Upgrading books from "%s" failed' % name) - self.newbibles[number].session.close() - del self.newbibles[number] + self.new_bibles[number].session.close() + del self.new_bibles[number] self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nFailed') % (number + 1, max_bibles, name), self.progress_bar.maximum() - self.progress_bar.value()) @@ -489,41 +489,41 @@ class BibleUpgradeForm(OpenLPWizard): self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nUpgrading %s ...') % (number + 1, max_bibles, name, book[u'name'])) - book_ref_id = self.newbibles[number].get_book_ref_id_by_name(book[u'name'], len(books), language_id) + book_ref_id = self.new_bibles[number].get_book_ref_id_by_name(book[u'name'], len(books), language_id) if not book_ref_id: log.warn(u'Upgrading books from %s " failed - aborted by user' % name) - self.newbibles[number].session.close() - del self.newbibles[number] + self.new_bibles[number].session.close() + del self.new_bibles[number] self.success[number] = False break book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - db_book = self.newbibles[number].create_book(book[u'name'], + db_book = self.new_bibles[number].create_book(book[u'name'], book_ref_id, book_details[u'testament_id']) verses = old_bible.get_verses(book[u'id']) if not verses: log.warn(u'No verses found to import for book "%s"', book[u'name']) - self.newbibles[number].delete_book(db_book) + self.new_bibles[number].delete_book(db_book) continue for verse in verses: if self.stop_import_flag: self.success[number] = False break - self.newbibles[number].create_verse(db_book.id, + self.new_bibles[number].create_verse(db_book.id, int(verse[u'chapter']), int(verse[u'verse']), unicode(verse[u'text'])) self.application.process_events() - self.newbibles[number].session.commit() + self.new_bibles[number].session.commit() if not self.success.get(number, True): self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nFailed') % (number + 1, max_bibles, name), self.progress_bar.maximum() - self.progress_bar.value()) else: self.success[number] = True - self.newbibles[number].save_meta(u'name', name) + self.new_bibles[number].save_meta(u'name', name) self.increment_progress_bar(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading Bible %s of %s: "%s"\nComplete') % (number + 1, max_bibles, name)) - if number in self.newbibles: - self.newbibles[number].session.close() + if number in self.new_bibles: + self.new_bibles[number].session.close() # Close the last bible's connection if possible. if old_bible is not None: old_bible.close_connection() diff --git a/openlp/plugins/bibles/forms/booknamedialog.py b/openlp/plugins/bibles/forms/booknamedialog.py index df86b4380..285e9cdcc 100644 --- a/openlp/plugins/bibles/forms/booknamedialog.py +++ b/openlp/plugins/bibles/forms/booknamedialog.py @@ -33,66 +33,66 @@ from openlp.core.lib import translate from openlp.core.lib.ui import create_button_box class Ui_BookNameDialog(object): - def setupUi(self, bookNameDialog): - bookNameDialog.setObjectName(u'bookNameDialog') - bookNameDialog.resize(400, 271) - self.bookNameLayout = QtGui.QVBoxLayout(bookNameDialog) - self.bookNameLayout.setSpacing(8) - self.bookNameLayout.setMargin(8) - self.bookNameLayout.setObjectName(u'bookNameLayout') - self.infoLabel = QtGui.QLabel(bookNameDialog) - self.infoLabel.setWordWrap(True) - self.infoLabel.setObjectName(u'infoLabel') - self.bookNameLayout.addWidget(self.infoLabel) - self.correspondingLayout = QtGui.QGridLayout() - self.correspondingLayout.setColumnStretch(1, 1) - self.correspondingLayout.setSpacing(8) - self.correspondingLayout.setObjectName(u'correspondingLayout') - self.currentLabel = QtGui.QLabel(bookNameDialog) + def setupUi(self, book_name_dialog): + book_name_dialog.setObjectName(u'book_name_dialog') + book_name_dialog.resize(400, 271) + self.book_name_layout = QtGui.QVBoxLayout(book_name_dialog) + self.book_name_layout.setSpacing(8) + self.book_name_layout.setMargin(8) + self.book_name_layout.setObjectName(u'book_name_layout') + self.info_label = QtGui.QLabel(book_name_dialog) + self.info_label.setWordWrap(True) + self.info_label.setObjectName(u'info_label') + self.book_name_layout.addWidget(self.info_label) + self.corresponding_layout = QtGui.QGridLayout() + self.corresponding_layout.setColumnStretch(1, 1) + self.corresponding_layout.setSpacing(8) + self.corresponding_layout.setObjectName(u'corresponding_layout') + self.currentLabel = QtGui.QLabel(book_name_dialog) self.currentLabel.setObjectName(u'currentLabel') - self.correspondingLayout.addWidget(self.currentLabel, 0, 0, 1, 1) - self.currentBookLabel = QtGui.QLabel(bookNameDialog) - self.currentBookLabel.setObjectName(u'currentBookLabel') - self.correspondingLayout.addWidget(self.currentBookLabel, 0, 1, 1, 1) - self.correspondingLabel = QtGui.QLabel(bookNameDialog) + self.corresponding_layout.addWidget(self.currentLabel, 0, 0, 1, 1) + self.current_book_label = QtGui.QLabel(book_name_dialog) + self.current_book_label.setObjectName(u'current_book_label') + self.corresponding_layout.addWidget(self.current_book_label, 0, 1, 1, 1) + self.correspondingLabel = QtGui.QLabel(book_name_dialog) self.correspondingLabel.setObjectName(u'correspondingLabel') - self.correspondingLayout.addWidget(self.correspondingLabel, 1, 0, 1, 1) - self.correspondingComboBox = QtGui.QComboBox(bookNameDialog) - self.correspondingComboBox.setObjectName(u'correspondingComboBox') - self.correspondingLayout.addWidget(self.correspondingComboBox, 1, 1, 1, 1) - self.bookNameLayout.addLayout(self.correspondingLayout) - self.optionsGroupBox = QtGui.QGroupBox(bookNameDialog) - self.optionsGroupBox.setObjectName(u'optionsGroupBox') - self.optionsLayout = QtGui.QVBoxLayout(self.optionsGroupBox) - self.optionsLayout.setSpacing(8) - self.optionsLayout.setMargin(8) - self.optionsLayout.setObjectName(u'optionsLayout') - self.oldTestamentCheckBox = QtGui.QCheckBox(self.optionsGroupBox) - self.oldTestamentCheckBox.setObjectName(u'oldTestamentCheckBox') - self.oldTestamentCheckBox.setCheckState(QtCore.Qt.Checked) - self.optionsLayout.addWidget(self.oldTestamentCheckBox) - self.newTestamentCheckBox = QtGui.QCheckBox(self.optionsGroupBox) - self.newTestamentCheckBox.setObjectName(u'newTestamentCheckBox') - self.newTestamentCheckBox.setCheckState(QtCore.Qt.Checked) - self.optionsLayout.addWidget(self.newTestamentCheckBox) - self.apocryphaCheckBox = QtGui.QCheckBox(self.optionsGroupBox) - self.apocryphaCheckBox.setObjectName(u'apocryphaCheckBox') - self.apocryphaCheckBox.setCheckState(QtCore.Qt.Checked) - self.optionsLayout.addWidget(self.apocryphaCheckBox) - self.bookNameLayout.addWidget(self.optionsGroupBox) - self.button_box = create_button_box(bookNameDialog, u'button_box', [u'cancel', u'ok']) - self.bookNameLayout.addWidget(self.button_box) + self.corresponding_layout.addWidget(self.correspondingLabel, 1, 0, 1, 1) + self.corresponding_combo_box = QtGui.QComboBox(book_name_dialog) + self.corresponding_combo_box.setObjectName(u'corresponding_combo_box') + self.corresponding_layout.addWidget(self.corresponding_combo_box, 1, 1, 1, 1) + self.book_name_layout.addLayout(self.corresponding_layout) + self.options_group_box = QtGui.QGroupBox(book_name_dialog) + self.options_group_box.setObjectName(u'options_group_box') + self.options_layout = QtGui.QVBoxLayout(self.options_group_box) + self.options_layout.setSpacing(8) + self.options_layout.setMargin(8) + self.options_layout.setObjectName(u'options_layout') + self.old_testament_check_box = QtGui.QCheckBox(self.options_group_box) + self.old_testament_check_box.setObjectName(u'old_testament_check_box') + self.old_testament_check_box.setCheckState(QtCore.Qt.Checked) + self.options_layout.addWidget(self.old_testament_check_box) + self.new_testament_check_box = QtGui.QCheckBox(self.options_group_box) + self.new_testament_check_box.setObjectName(u'new_testament_check_box') + self.new_testament_check_box.setCheckState(QtCore.Qt.Checked) + self.options_layout.addWidget(self.new_testament_check_box) + self.apocrypha_check_box = QtGui.QCheckBox(self.options_group_box) + self.apocrypha_check_box.setObjectName(u'apocrypha_check_box') + self.apocrypha_check_box.setCheckState(QtCore.Qt.Checked) + self.options_layout.addWidget(self.apocrypha_check_box) + self.book_name_layout.addWidget(self.options_group_box) + self.button_box = create_button_box(book_name_dialog, u'button_box', [u'cancel', u'ok']) + self.book_name_layout.addWidget(self.button_box) - self.retranslateUi(bookNameDialog) + self.retranslateUi(book_name_dialog) - def retranslateUi(self, bookNameDialog): - bookNameDialog.setWindowTitle(translate('BiblesPlugin.BookNameDialog', 'Select Book Name')) - self.infoLabel.setText(translate('BiblesPlugin.BookNameDialog', + def retranslateUi(self, book_name_dialog): + book_name_dialog.setWindowTitle(translate('BiblesPlugin.BookNameDialog', 'Select Book Name')) + self.info_label.setText(translate('BiblesPlugin.BookNameDialog', 'The following book name cannot be matched up internally. ' 'Please select the corresponding name from the list.')) self.currentLabel.setText(translate('BiblesPlugin.BookNameDialog', 'Current name:')) self.correspondingLabel.setText(translate('BiblesPlugin.BookNameDialog', 'Corresponding name:')) - self.optionsGroupBox.setTitle(translate('BiblesPlugin.BookNameDialog', 'Show Books From')) - self.oldTestamentCheckBox.setText(translate('BiblesPlugin.BookNameDialog', 'Old Testament')) - self.newTestamentCheckBox.setText(translate('BiblesPlugin.BookNameDialog', 'New Testament')) - self.apocryphaCheckBox.setText(translate('BiblesPlugin.BookNameDialog', 'Apocrypha')) + self.options_group_box.setTitle(translate('BiblesPlugin.BookNameDialog', 'Show Books From')) + self.old_testament_check_box.setText(translate('BiblesPlugin.BookNameDialog', 'Old Testament')) + self.new_testament_check_box.setText(translate('BiblesPlugin.BookNameDialog', 'New Testament')) + self.apocrypha_check_box.setText(translate('BiblesPlugin.BookNameDialog', 'Apocrypha')) diff --git a/openlp/plugins/bibles/forms/booknameform.py b/openlp/plugins/bibles/forms/booknameform.py index c8abaa227..cc213cf96 100644 --- a/openlp/plugins/bibles/forms/booknameform.py +++ b/openlp/plugins/bibles/forms/booknameform.py @@ -66,61 +66,61 @@ class BookNameForm(QDialog, Ui_BookNameDialog): """ Set up the signals used in the booknameform. """ - self.oldTestamentCheckBox.stateChanged.connect(self.onCheckBoxIndexChanged) - self.newTestamentCheckBox.stateChanged.connect(self.onCheckBoxIndexChanged) - self.apocryphaCheckBox.stateChanged.connect(self.onCheckBoxIndexChanged) + self.old_testament_check_box.stateChanged.connect(self.onCheckBoxIndexChanged) + self.new_testament_check_box.stateChanged.connect(self.onCheckBoxIndexChanged) + self.apocrypha_check_box.stateChanged.connect(self.onCheckBoxIndexChanged) def onCheckBoxIndexChanged(self, index): """ Reload Combobox if CheckBox state has changed """ - self.reloadComboBox() + self.reload_combo_box() - def reloadComboBox(self): + def reload_combo_box(self): """ Reload the Combobox items """ - self.correspondingComboBox.clear() + self.corresponding_combo_box.clear() items = BiblesResourcesDB.get_books() for item in items: - addBook = True + add_book = True for book in self.books: if book.book_reference_id == item[u'id']: - addBook = False + add_book = False break - if self.oldTestamentCheckBox.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 1: - addBook = False - elif self.newTestamentCheckBox.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 2: - addBook = False - elif self.apocryphaCheckBox.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 3: - addBook = False - if addBook: - self.correspondingComboBox.addItem(self.book_names[item[u'abbreviation']]) + if self.old_testament_check_box.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 1: + add_book = False + elif self.new_testament_check_box.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 2: + add_book = False + elif self.apocrypha_check_box.checkState() == QtCore.Qt.Unchecked and item[u'testament_id'] == 3: + add_book = False + if add_book: + self.corresponding_combo_box.addItem(self.book_names[item[u'abbreviation']]) - def exec_(self, name, books, maxbooks): + def exec_(self, name, books, max_books): self.books = books - log.debug(maxbooks) - if maxbooks <= 27: - self.oldTestamentCheckBox.setCheckState(QtCore.Qt.Unchecked) - self.apocryphaCheckBox.setCheckState(QtCore.Qt.Unchecked) - elif maxbooks <= 66: - self.apocryphaCheckBox.setCheckState(QtCore.Qt.Unchecked) - self.reloadComboBox() - self.currentBookLabel.setText(unicode(name)) - self.correspondingComboBox.setFocus() + log.debug(max_books) + if max_books <= 27: + self.old_testament_check_box.setCheckState(QtCore.Qt.Unchecked) + self.apocrypha_check_box.setCheckState(QtCore.Qt.Unchecked) + elif max_books <= 66: + self.apocrypha_check_box.setCheckState(QtCore.Qt.Unchecked) + self.reload_combo_box() + self.current_book_label.setText(unicode(name)) + self.corresponding_combo_box.setFocus() return QDialog.exec_(self) def accept(self): - if self.correspondingComboBox.currentText() == u'': + if not self.corresponding_combo_box.currentText(): critical_error_message_box(message=translate('BiblesPlugin.BookNameForm', 'You need to select a book.')) - self.correspondingComboBox.setFocus() + self.corresponding_combo_box.setFocus() return False else: - cor_book = self.correspondingComboBox.currentText() + cor_book = self.corresponding_combo_box.currentText() for character in u'\\.^$*+?{}[]()': cor_book = cor_book.replace(character, u'\\' + character) - books = filter(lambda key: - re.match(cor_book, unicode(self.book_names[key]), re.UNICODE), self.book_names.keys()) + books = filter( + lambda key: re.match(cor_book, unicode(self.book_names[key]), re.UNICODE), self.book_names.keys()) books = filter(None, map(BiblesResourcesDB.get_book, books)) if books: self.book_id = books[0][u'id'] diff --git a/openlp/plugins/bibles/forms/editbibledialog.py b/openlp/plugins/bibles/forms/editbibledialog.py index b45bb2751..79cef2575 100644 --- a/openlp/plugins/bibles/forms/editbibledialog.py +++ b/openlp/plugins/bibles/forms/editbibledialog.py @@ -36,118 +36,118 @@ from openlp.plugins.bibles.lib.db import BiblesResourcesDB class Ui_EditBibleDialog(object): - def setupUi(self, editBibleDialog): - editBibleDialog.setObjectName(u'editBibleDialog') - editBibleDialog.resize(520, 400) - editBibleDialog.setWindowIcon(build_icon(u':/icon/openlp-logo-16x16.png')) - editBibleDialog.setModal(True) - self.dialogLayout = QtGui.QVBoxLayout(editBibleDialog) - self.dialogLayout.setSpacing(8) - self.dialogLayout.setContentsMargins(8, 8, 8, 8) - self.dialogLayout.setObjectName(u'dialog_layout') - self.bibleTabWidget = QtGui.QTabWidget(editBibleDialog) - self.bibleTabWidget.setObjectName(u'BibleTabWidget') + def setupUi(self, edit_bible_dialog): + edit_bible_dialog.setObjectName(u'edit_bible_dialog') + edit_bible_dialog.resize(520, 400) + edit_bible_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo-16x16.png')) + edit_bible_dialog.setModal(True) + self.dialog_layout = QtGui.QVBoxLayout(edit_bible_dialog) + self.dialog_layout.setSpacing(8) + self.dialog_layout.setContentsMargins(8, 8, 8, 8) + self.dialog_layout.setObjectName(u'dialog_layout') + self.bible_tab_widget = QtGui.QTabWidget(edit_bible_dialog) + self.bible_tab_widget.setObjectName(u'BibleTabWidget') # Meta tab - self.metaTab = QtGui.QWidget() - self.metaTab.setObjectName(u'metaTab') - self.metaTabLayout = QtGui.QVBoxLayout(self.metaTab) - self.metaTabLayout.setObjectName(u'metaTabLayout') - self.licenseDetailsGroupBox = QtGui.QGroupBox(self.metaTab) - self.licenseDetailsGroupBox.setObjectName(u'licenseDetailsGroupBox') - self.licenseDetailsLayout = QtGui.QFormLayout(self.licenseDetailsGroupBox) - self.licenseDetailsLayout.setObjectName(u'licenseDetailsLayout') - self.versionNameLabel = QtGui.QLabel(self.licenseDetailsGroupBox) - self.versionNameLabel.setObjectName(u'versionNameLabel') - self.versionNameEdit = QtGui.QLineEdit(self.licenseDetailsGroupBox) - self.versionNameEdit.setObjectName(u'versionNameEdit') - self.versionNameLabel.setBuddy(self.versionNameEdit) - self.licenseDetailsLayout.addRow(self.versionNameLabel, self.versionNameEdit) - self.copyrightLabel = QtGui.QLabel(self.licenseDetailsGroupBox) - self.copyrightLabel.setObjectName(u'copyrightLabel') - self.copyrightEdit = QtGui.QLineEdit(self.licenseDetailsGroupBox) - self.copyrightEdit.setObjectName(u'copyright_edit') - self.copyrightLabel.setBuddy(self.copyrightEdit) - self.licenseDetailsLayout.addRow(self.copyrightLabel, self.copyrightEdit) - self.permissionsLabel = QtGui.QLabel(self.licenseDetailsGroupBox) - self.permissionsLabel.setObjectName(u'permissionsLabel') - self.permissionsEdit = QtGui.QLineEdit(self.licenseDetailsGroupBox) - self.permissionsEdit.setObjectName(u'permissionsEdit') - self.permissionsLabel.setBuddy(self.permissionsEdit) - self.licenseDetailsLayout.addRow(self.permissionsLabel, self.permissionsEdit) - self.metaTabLayout.addWidget(self.licenseDetailsGroupBox) - self.languageSelectionGroupBox = QtGui.QGroupBox(self.metaTab) - self.languageSelectionGroupBox.setObjectName(u'languageSelectionGroupBox') - self.languageSelectionLayout = QtGui.QVBoxLayout(self.languageSelectionGroupBox) - self.languageSelectionLabel = QtGui.QLabel(self.languageSelectionGroupBox) - self.languageSelectionLabel.setObjectName(u'languageSelectionLabel') - self.languageSelectionComboBox = QtGui.QComboBox(self.languageSelectionGroupBox) - self.languageSelectionComboBox.setObjectName(u'languageSelectionComboBox') - self.languageSelectionComboBox.addItems([u'', u'', u'', u'']) - self.languageSelectionLayout.addWidget(self.languageSelectionLabel) - self.languageSelectionLayout.addWidget(self.languageSelectionComboBox) - self.metaTabLayout.addWidget(self.languageSelectionGroupBox) - self.metaTabLayout.addStretch() - self.bibleTabWidget.addTab(self.metaTab, u'') + self.meta_tab = QtGui.QWidget() + self.meta_tab.setObjectName(u'meta_tab') + self.meta_tab_layout = QtGui.QVBoxLayout(self.meta_tab) + self.meta_tab_layout.setObjectName(u'meta_tab_layout') + self.license_details_group_box = QtGui.QGroupBox(self.meta_tab) + self.license_details_group_box.setObjectName(u'license_details_group_box') + self.license_details_layout = QtGui.QFormLayout(self.license_details_group_box) + self.license_details_layout.setObjectName(u'license_details_layout') + self.version_name_label = QtGui.QLabel(self.license_details_group_box) + self.version_name_label.setObjectName(u'version_name_label') + self.version_name_edit = QtGui.QLineEdit(self.license_details_group_box) + self.version_name_edit.setObjectName(u'version_name_edit') + self.version_name_label.setBuddy(self.version_name_edit) + self.license_details_layout.addRow(self.version_name_label, self.version_name_edit) + self.copyright_label = QtGui.QLabel(self.license_details_group_box) + self.copyright_label.setObjectName(u'copyright_label') + self.copyright_edit = QtGui.QLineEdit(self.license_details_group_box) + self.copyright_edit.setObjectName(u'copyright_edit') + self.copyright_label.setBuddy(self.copyright_edit) + self.license_details_layout.addRow(self.copyright_label, self.copyright_edit) + self.permissions_label = QtGui.QLabel(self.license_details_group_box) + self.permissions_label.setObjectName(u'permissions_label') + self.permissions_edit = QtGui.QLineEdit(self.license_details_group_box) + self.permissions_edit.setObjectName(u'permissions_edit') + self.permissions_label.setBuddy(self.permissions_edit) + self.license_details_layout.addRow(self.permissions_label, self.permissions_edit) + self.meta_tab_layout.addWidget(self.license_details_group_box) + self.language_selection_group_box = QtGui.QGroupBox(self.meta_tab) + self.language_selection_group_box.setObjectName(u'language_selection_group_box') + self.language_selection_layout = QtGui.QVBoxLayout(self.language_selection_group_box) + self.language_selection_label = QtGui.QLabel(self.language_selection_group_box) + self.language_selection_label.setObjectName(u'language_selection_label') + self.language_selection_combo_box = QtGui.QComboBox(self.language_selection_group_box) + self.language_selection_combo_box.setObjectName(u'language_selection_combo_box') + self.language_selection_combo_box.addItems([u'', u'', u'', u'']) + self.language_selection_layout.addWidget(self.language_selection_label) + self.language_selection_layout.addWidget(self.language_selection_combo_box) + self.meta_tab_layout.addWidget(self.language_selection_group_box) + self.meta_tab_layout.addStretch() + self.bible_tab_widget.addTab(self.meta_tab, u'') # Book name tab - self.bookNameTab = QtGui.QWidget() - self.bookNameTab.setObjectName(u'bookNameTab') - self.bookNameTabLayout = QtGui.QVBoxLayout(self.bookNameTab) - self.bookNameTabLayout.setObjectName(u'bookNameTabLayout') - self.bookNameNotice = QtGui.QLabel(self.bookNameTab) - self.bookNameNotice.setObjectName(u'bookNameNotice') - self.bookNameNotice.setWordWrap(True) - self.bookNameTabLayout.addWidget(self.bookNameNotice) - self.scrollArea = QtGui.QScrollArea(self.bookNameTab) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setObjectName(u'scrollArea') - self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.bookNameWidget = QtGui.QWidget(self.scrollArea) - self.bookNameWidget.setObjectName(u'bookNameWidget') - self.bookNameWidgetLayout = QtGui.QFormLayout(self.bookNameWidget) - self.bookNameWidgetLayout.setObjectName(u'bookNameWidgetLayout') - self.bookNameLabel = {} - self.bookNameEdit= {} + self.book_name_tab = QtGui.QWidget() + self.book_name_tab.setObjectName(u'book_name_tab') + self.book_name_tab_layout = QtGui.QVBoxLayout(self.book_name_tab) + self.book_name_tab_layout.setObjectName(u'book_name_tab_layout') + self.book_name_notice = QtGui.QLabel(self.book_name_tab) + self.book_name_notice.setObjectName(u'book_name_notice') + self.book_name_notice.setWordWrap(True) + self.book_name_tab_layout.addWidget(self.book_name_notice) + self.scroll_area = QtGui.QScrollArea(self.book_name_tab) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setObjectName(u'scroll_area') + self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.book_name_widget = QtGui.QWidget(self.scroll_area) + self.book_name_widget.setObjectName(u'book_name_widget') + self.book_name_widget_layout = QtGui.QFormLayout(self.book_name_widget) + self.book_name_widget_layout.setObjectName(u'book_name_widget_layout') + self.book_name_label = {} + self.book_name_edit= {} for book in BiblesResourcesDB.get_books(): - self.bookNameLabel[book[u'abbreviation']] = QtGui.QLabel(self.bookNameWidget) - self.bookNameLabel[book[u'abbreviation']].setObjectName(u'bookNameLabel[%s]' % book[u'abbreviation']) - self.bookNameEdit[book[u'abbreviation']] = QtGui.QLineEdit(self.bookNameWidget) - self.bookNameEdit[book[u'abbreviation']].setObjectName(u'bookNameEdit[%s]' % book[u'abbreviation']) - self.bookNameWidgetLayout.addRow( - self.bookNameLabel[book[u'abbreviation']], - self.bookNameEdit[book[u'abbreviation']]) - self.scrollArea.setWidget(self.bookNameWidget) - self.bookNameTabLayout.addWidget(self.scrollArea) - self.bookNameTabLayout.addStretch() - self.bibleTabWidget.addTab(self.bookNameTab, u'') + self.book_name_label[book[u'abbreviation']] = QtGui.QLabel(self.book_name_widget) + self.book_name_label[book[u'abbreviation']].setObjectName(u'book_name_label[%s]' % book[u'abbreviation']) + self.book_name_edit[book[u'abbreviation']] = QtGui.QLineEdit(self.book_name_widget) + self.book_name_edit[book[u'abbreviation']].setObjectName(u'book_name_edit[%s]' % book[u'abbreviation']) + self.book_name_widget_layout.addRow( + self.book_name_label[book[u'abbreviation']], + self.book_name_edit[book[u'abbreviation']]) + self.scroll_area.setWidget(self.book_name_widget) + self.book_name_tab_layout.addWidget(self.scroll_area) + self.book_name_tab_layout.addStretch() + self.bible_tab_widget.addTab(self.book_name_tab, u'') # Last few bits - self.dialogLayout.addWidget(self.bibleTabWidget) - self.button_box = create_button_box(editBibleDialog, u'button_box', [u'cancel', u'save']) - self.dialogLayout.addWidget(self.button_box) - self.retranslateUi(editBibleDialog) - QtCore.QMetaObject.connectSlotsByName(editBibleDialog) + self.dialog_layout.addWidget(self.bible_tab_widget) + self.button_box = create_button_box(edit_bible_dialog, u'button_box', [u'cancel', u'save']) + self.dialog_layout.addWidget(self.button_box) + self.retranslateUi(edit_bible_dialog) + QtCore.QMetaObject.connectSlotsByName(edit_bible_dialog) - def retranslateUi(self, editBibleDialog): + def retranslateUi(self, edit_bible_dialog): self.book_names = BibleStrings().BookNames - editBibleDialog.setWindowTitle(translate('BiblesPlugin.EditBibleForm', 'Bible Editor')) + edit_bible_dialog.setWindowTitle(translate('BiblesPlugin.EditBibleForm', 'Bible Editor')) # Meta tab - self.bibleTabWidget.setTabText( self.bibleTabWidget.indexOf(self.metaTab), + self.bible_tab_widget.setTabText( self.bible_tab_widget.indexOf(self.meta_tab), translate('SongsPlugin.EditBibleForm', 'Meta Data')) - self.licenseDetailsGroupBox.setTitle(translate('BiblesPlugin.EditBibleForm', 'License Details')) - self.versionNameLabel.setText(translate('BiblesPlugin.EditBibleForm', 'Version name:')) - self.copyrightLabel.setText(translate('BiblesPlugin.EditBibleForm', 'Copyright:')) - self.permissionsLabel.setText(translate('BiblesPlugin.EditBibleForm', 'Permissions:')) - self.languageSelectionGroupBox.setTitle(translate('BiblesPlugin.EditBibleForm', 'Default Bible Language')) - self.languageSelectionLabel.setText(translate('BiblesPlugin.EditBibleForm', + self.license_details_group_box.setTitle(translate('BiblesPlugin.EditBibleForm', 'License Details')) + self.version_name_label.setText(translate('BiblesPlugin.EditBibleForm', 'Version name:')) + self.copyright_label.setText(translate('BiblesPlugin.EditBibleForm', 'Copyright:')) + self.permissions_label.setText(translate('BiblesPlugin.EditBibleForm', 'Permissions:')) + self.language_selection_group_box.setTitle(translate('BiblesPlugin.EditBibleForm', 'Default Bible Language')) + self.language_selection_label.setText(translate('BiblesPlugin.EditBibleForm', 'Book name language in search field, search results and on display:')) - self.languageSelectionComboBox.setItemText(0, translate('BiblesPlugin.EditBibleForm', 'Global Settings')) - self.languageSelectionComboBox.setItemText(LanguageSelection.Bible + 1, + self.language_selection_combo_box.setItemText(0, translate('BiblesPlugin.EditBibleForm', 'Global Settings')) + self.language_selection_combo_box.setItemText(LanguageSelection.Bible + 1, translate('BiblesPlugin.EditBibleForm', 'Bible Language')) - self.languageSelectionComboBox.setItemText(LanguageSelection.Application + 1, + self.language_selection_combo_box.setItemText(LanguageSelection.Application + 1, translate('BiblesPlugin.EditBibleForm', 'Application Language')) - self.languageSelectionComboBox.setItemText(LanguageSelection.English + 1, + self.language_selection_combo_box.setItemText(LanguageSelection.English + 1, translate('BiblesPlugin.EditBibleForm', 'English')) # Book name tab - self.bibleTabWidget.setTabText(self.bibleTabWidget.indexOf(self.bookNameTab), + self.bible_tab_widget.setTabText(self.bible_tab_widget.indexOf(self.book_name_tab), translate('SongsPlugin.EditBibleForm', 'Custom Book Names')) for book in BiblesResourcesDB.get_books(): - self.bookNameLabel[book[u'abbreviation']].setText(u'%s:' % unicode(self.book_names[book[u'abbreviation']])) + self.book_name_label[book[u'abbreviation']].setText(u'%s:' % unicode(self.book_names[book[u'abbreviation']])) diff --git a/openlp/plugins/bibles/forms/editbibleform.py b/openlp/plugins/bibles/forms/editbibleform.py index cbb02109a..b05fb68fb 100644 --- a/openlp/plugins/bibles/forms/editbibleform.py +++ b/openlp/plugins/bibles/forms/editbibleform.py @@ -65,33 +65,33 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): """ log.debug(u'Load Bible') self.bible = bible - self.versionNameEdit.setText(self.manager.get_meta_data(self.bible, u'name').value) - self.copyrightEdit.setText(self.manager.get_meta_data(self.bible, u'copyright').value) - self.permissionsEdit.setText(self.manager.get_meta_data(self.bible, u'permissions').value) + self.version_name_edit.setText(self.manager.get_meta_data(self.bible, u'name').value) + self.copyright_edit.setText(self.manager.get_meta_data(self.bible, u'copyright').value) + self.permissions_edit.setText(self.manager.get_meta_data(self.bible, u'permissions').value) book_name_language = self.manager.get_meta_data(self.bible, u'book_name_language') if book_name_language and book_name_language.value != u'None': - self.languageSelectionComboBox.setCurrentIndex(int(book_name_language.value) + 1) + self.language_selection_combo_box.setCurrentIndex(int(book_name_language.value) + 1) self.books = {} self.webbible = self.manager.get_meta_data(self.bible, u'download_source') if self.webbible: - self.bookNameNotice.setText(translate('BiblesPlugin.EditBibleForm', + self.book_name_notice.setText(translate('BiblesPlugin.EditBibleForm', 'This is a Web Download Bible.\nIt is not possible to customize the Book Names.')) - self.scrollArea.hide() + self.scroll_area.hide() else: - self.bookNameNotice.setText(translate('BiblesPlugin.EditBibleForm', + self.book_name_notice.setText(translate('BiblesPlugin.EditBibleForm', 'To use the customized book names, "Bible language" must be selected on the Meta Data tab or, ' 'if "Global settings" is selected, on the Bible page in Configure OpenLP.')) for book in BiblesResourcesDB.get_books(): self.books[book[u'abbreviation']] = self.manager.get_book_by_id(self.bible, book[u'id']) if self.books[book[u'abbreviation']] and not self.webbible: - self.bookNameEdit[book[u'abbreviation']].setText(self.books[book[u'abbreviation']].name) + self.book_name_edit[book[u'abbreviation']].setText(self.books[book[u'abbreviation']].name) else: # It is necessary to remove the Widget otherwise there still # exists the vertical spacing in QFormLayout - self.bookNameWidgetLayout.removeWidget(self.bookNameLabel[book[u'abbreviation']]) - self.bookNameLabel[book[u'abbreviation']].hide() - self.bookNameWidgetLayout.removeWidget(self.bookNameEdit[book[u'abbreviation']]) - self.bookNameEdit[book[u'abbreviation']].hide() + self.book_name_widget_layout.removeWidget(self.book_name_label[book[u'abbreviation']]) + self.book_name_label[book[u'abbreviation']].hide() + self.book_name_widget_layout.removeWidget(self.book_name_edit[book[u'abbreviation']]) + self.book_name_edit[book[u'abbreviation']].hide() def reject(self): """ @@ -106,10 +106,10 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): Exit Dialog and save data """ log.debug(u'BibleEditForm.accept') - version = self.versionNameEdit.text() - copyright = self.copyrightEdit.text() - permissions = self.permissionsEdit.text() - book_name_language = self.languageSelectionComboBox.currentIndex() - 1 + version = self.version_name_edit.text() + copyright = self.copyright_edit.text() + permissions = self.permissions_edit.text() + book_name_language = self.language_selection_combo_box.currentIndex() - 1 if book_name_language == -1: book_name_language = None if not self.validateMeta(version, copyright): @@ -118,7 +118,7 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): custom_names = {} for abbr, book in self.books.iteritems(): if book: - custom_names[abbr] = self.bookNameEdit[abbr].text() + custom_names[abbr] = self.book_name_edit[abbr].text() if book.name != custom_names[abbr]: if not self.validateBook(custom_names[abbr], abbr): return @@ -139,19 +139,19 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): Validate the Meta before saving. """ if not name: - self.versionNameEdit.setFocus() + self.version_name_edit.setFocus() critical_error_message_box(UiStrings().EmptyField, translate('BiblesPlugin.BibleEditForm', 'You need to specify a version name for your Bible.')) return False elif not copyright: - self.copyrightEdit.setFocus() + self.copyright_edit.setFocus() critical_error_message_box(UiStrings().EmptyField, translate('BiblesPlugin.BibleEditForm', 'You need to set a copyright for your Bible. Bibles in the Public Domain need to be marked as such.')) return False elif self.manager.exists(name) and self.manager.get_meta_data(self.bible, u'name').value != \ name: - self.versionNameEdit.setFocus() + self.version_name_edit.setFocus() critical_error_message_box(translate('BiblesPlugin.BibleEditForm', 'Bible Exists'), translate('BiblesPlugin.BibleEditForm', 'This Bible already exists. Please import ' 'a different Bible or first delete the existing one.')) @@ -164,13 +164,13 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): """ book_regex = re.compile(u'[\d]*[^\d]+$') if not new_book_name: - self.bookNameEdit[abbreviation].setFocus() + self.book_name_edit[abbreviation].setFocus() critical_error_message_box(UiStrings().EmptyField, translate('BiblesPlugin.BibleEditForm', 'You need to specify a book name for "%s".') % self.book_names[abbreviation]) return False elif not book_regex.match(new_book_name): - self.bookNameEdit[abbreviation].setFocus() + self.book_name_edit[abbreviation].setFocus() critical_error_message_box(UiStrings().EmptyField, translate('BiblesPlugin.BibleEditForm', 'The book name "%s" is not correct.\nNumbers can only be used at the beginning and must\nbe ' @@ -180,8 +180,8 @@ class EditBibleForm(QtGui.QDialog, Ui_EditBibleDialog): if book: if abbr == abbreviation: continue - if self.bookNameEdit[abbr].text() == new_book_name: - self.bookNameEdit[abbreviation].setFocus() + if self.book_name_edit[abbr].text() == new_book_name: + self.book_name_edit[abbreviation].setFocus() critical_error_message_box( translate('BiblesPlugin.BibleEditForm', 'Duplicate Book Name'), translate('BiblesPlugin.BibleEditForm', 'The Book Name "%s" has been entered more than once.') diff --git a/openlp/plugins/bibles/forms/languagedialog.py b/openlp/plugins/bibles/forms/languagedialog.py index 9ad16bb30..870957c3c 100644 --- a/openlp/plugins/bibles/forms/languagedialog.py +++ b/openlp/plugins/bibles/forms/languagedialog.py @@ -33,44 +33,44 @@ from openlp.core.lib import translate from openlp.core.lib.ui import create_button_box class Ui_LanguageDialog(object): - def setupUi(self, languageDialog): - languageDialog.setObjectName(u'languageDialog') - languageDialog.resize(400, 165) - self.languageLayout = QtGui.QVBoxLayout(languageDialog) - self.languageLayout.setSpacing(8) - self.languageLayout.setMargin(8) - self.languageLayout.setObjectName(u'languageLayout') - self.bibleLabel = QtGui.QLabel(languageDialog) - self.bibleLabel.setObjectName(u'bibleLabel') - self.languageLayout.addWidget(self.bibleLabel) - self.infoLabel = QtGui.QLabel(languageDialog) - self.infoLabel.setWordWrap(True) - self.infoLabel.setObjectName(u'infoLabel') - self.languageLayout.addWidget(self.infoLabel) - self.languageHBoxLayout = QtGui.QHBoxLayout() - self.languageHBoxLayout.setSpacing(8) - self.languageHBoxLayout.setObjectName(u'languageHBoxLayout') - self.languageLabel = QtGui.QLabel(languageDialog) - self.languageLabel.setObjectName(u'languageLabel') - self.languageHBoxLayout.addWidget(self.languageLabel) - self.languageComboBox = QtGui.QComboBox(languageDialog) + def setupUi(self, language_dialog): + language_dialog.setObjectName(u'language_dialog') + language_dialog.resize(400, 165) + self.language_layout = QtGui.QVBoxLayout(language_dialog) + self.language_layout.setSpacing(8) + self.language_layout.setMargin(8) + self.language_layout.setObjectName(u'language_layout') + self.bible_label = QtGui.QLabel(language_dialog) + self.bible_label.setObjectName(u'bible_label') + self.language_layout.addWidget(self.bible_label) + self.info_label = QtGui.QLabel(language_dialog) + self.info_label.setWordWrap(True) + self.info_label.setObjectName(u'info_label') + self.language_layout.addWidget(self.info_label) + self.language_h_box_layout = QtGui.QHBoxLayout() + self.language_h_box_layout.setSpacing(8) + self.language_h_box_layout.setObjectName(u'language_h_box_layout') + self.language_label = QtGui.QLabel(language_dialog) + self.language_label.setObjectName(u'language_label') + self.language_h_box_layout.addWidget(self.language_label) + self.language_combo_box = QtGui.QComboBox(language_dialog) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.languageComboBox.sizePolicy().hasHeightForWidth()) - self.languageComboBox.setSizePolicy(sizePolicy) - self.languageComboBox.setObjectName(u'languageComboBox') - self.languageHBoxLayout.addWidget(self.languageComboBox) - self.languageLayout.addLayout(self.languageHBoxLayout) - self.button_box = create_button_box(languageDialog, u'button_box', [u'cancel', u'ok']) - self.languageLayout.addWidget(self.button_box) + sizePolicy.setHeightForWidth(self.language_combo_box.sizePolicy().hasHeightForWidth()) + self.language_combo_box.setSizePolicy(sizePolicy) + self.language_combo_box.setObjectName(u'language_combo_box') + self.language_h_box_layout.addWidget(self.language_combo_box) + self.language_layout.addLayout(self.language_h_box_layout) + self.button_box = create_button_box(language_dialog, u'button_box', [u'cancel', u'ok']) + self.language_layout.addWidget(self.button_box) - self.retranslateUi(languageDialog) + self.retranslateUi(language_dialog) - def retranslateUi(self, languageDialog): - languageDialog.setWindowTitle(translate('BiblesPlugin.LanguageDialog', 'Select Language')) - self.bibleLabel.setText(translate('BiblesPlugin.LanguageDialog', '')) - self.infoLabel.setText(translate('BiblesPlugin.LanguageDialog', + def retranslateUi(self, language_dialog): + language_dialog.setWindowTitle(translate('BiblesPlugin.LanguageDialog', 'Select Language')) + self.bible_label.setText(translate('BiblesPlugin.LanguageDialog', '')) + self.info_label.setText(translate('BiblesPlugin.LanguageDialog', 'OpenLP is unable to determine the language of this translation of the Bible. Please select the language ' 'from the list below.')) - self.languageLabel.setText(translate('BiblesPlugin.LanguageDialog', 'Language:')) + self.language_label.setText(translate('BiblesPlugin.LanguageDialog', 'Language:')) diff --git a/openlp/plugins/bibles/forms/languageform.py b/openlp/plugins/bibles/forms/languageform.py index cdac6dec9..2bba3d7a4 100644 --- a/openlp/plugins/bibles/forms/languageform.py +++ b/openlp/plugins/bibles/forms/languageform.py @@ -40,8 +40,10 @@ from openlp.plugins.bibles.forms.languagedialog import \ Ui_LanguageDialog from openlp.plugins.bibles.lib.db import BiblesResourcesDB + log = logging.getLogger(__name__) + class LanguageForm(QDialog, Ui_LanguageDialog): """ Class to manage a dialog which ask the user for a language. @@ -56,19 +58,17 @@ class LanguageForm(QDialog, Ui_LanguageDialog): self.setupUi(self) def exec_(self, bible_name): - self.languageComboBox.addItem(u'') + self.language_combo_box.addItem(u'') if bible_name: - self.bibleLabel.setText(unicode(bible_name)) + self.bible_label.setText(unicode(bible_name)) items = BiblesResourcesDB.get_languages() - self.languageComboBox.addItems([item[u'name'] for item in items]) + self.language_combo_box.addItems([item[u'name'] for item in items]) return QDialog.exec_(self) def accept(self): - if not self.languageComboBox.currentText(): - critical_error_message_box( - message=translate('BiblesPlugin.LanguageForm', - 'You need to choose a language.')) - self.languageComboBox.setFocus() + if not self.language_combo_box.currentText(): + critical_error_message_box(message=translate('BiblesPlugin.LanguageForm', 'You need to choose a language.')) + self.language_combo_box.setFocus() return False else: return QDialog.accept(self) diff --git a/openlp/plugins/bibles/lib/__init__.py b/openlp/plugins/bibles/lib/__init__.py index 219b91f90..371632d41 100644 --- a/openlp/plugins/bibles/lib/__init__.py +++ b/openlp/plugins/bibles/lib/__init__.py @@ -38,9 +38,11 @@ from openlp.core.lib import Settings, translate log = logging.getLogger(__name__) + REFERENCE_MATCHES = {} REFERENCE_SEPARATORS = {} + class LayoutStyle(object): """ An enumeration for bible screen layout styles. @@ -62,8 +64,7 @@ class DisplayStyle(object): class LanguageSelection(object): """ - An enumeration for bible bookname language. - And standard strings for use throughout the bibles plugin. + An enumeration for bible bookname language. And standard strings for use throughout the bibles plugin. """ Bible = 0 Application = 1 @@ -178,8 +179,7 @@ class BibleStrings(object): def update_reference_separators(): """ - Updates separators and matches for parsing and formating scripture - references. + Updates separators and matches for parsing and formating scripture references. """ default_separators = translate('BiblesPlugin', ':|v|V|verse|verses;;-|to;;,|and;;end Double-semicolon delimited separators for parsing references. ' @@ -245,9 +245,8 @@ def get_reference_match(match_type): def parse_reference(reference, bible, language_selection, book_ref_id=False): """ - This is the next generation über-awesome function that takes a person's - typed in string and converts it to a list of references to be queried from - the Bible database files. + This is the next generation über-awesome function that takes a person's typed in string and converts it to a list + of references to be queried from the Bible database files. ``reference`` A string. The Bible reference to parse. @@ -256,16 +255,14 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): A object. The Bible database object. ``language_selection`` - An int. The language selection the user has choosen in settings - section. + An int. The language selection the user has choosen in settings section. ``book_ref_id`` A string. The book reference id. Returns ``None`` or a reference list. - The reference list is a list of tuples, with each tuple structured like - this:: + The reference list is a list of tuples, with each tuple structured like this:: (book, chapter, from_verse, to_verse) @@ -275,8 +272,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): **Reference string details:** - Each reference starts with the book name and a chapter number. These are - both mandatory. + Each reference starts with the book name and a chapter number. These are both mandatory. * ``John 3`` refers to Gospel of John chapter 3 @@ -289,38 +285,31 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): * ``John 3:16`` refers to John chapter 3 verse 16 * ``John 3:16-4:3`` refers to John chapter 3 verse 16 to chapter 4 verse 3 - After a verse reference all further single values are treat as verse in - the last selected chapter. + After a verse reference all further single values are treat as verse in the last selected chapter. * ``John 3:16-18`` refers to John chapter 3 verses 16 to 18 - After a list separator it is possible to refer to additional verses. They - are build analog to the first ones. This way it is possible to define each - number of verse references. It is not possible to refer to verses in - additional books. + After a list separator it is possible to refer to additional verses. They are build analog to the first ones. This + way it is possible to define each number of verse references. It is not possible to refer to verses in additional + books. * ``John 3:16,18`` refers to John chapter 3 verses 16 and 18 * ``John 3:16-18,20`` refers to John chapter 3 verses 16 to 18 and 20 - * ``John 3:16-18,4:1`` refers to John chapter 3 verses 16 to 18 and - chapter 4 verse 1 + * ``John 3:16-18,4:1`` refers to John chapter 3 verses 16 to 18 and chapter 4 verse 1 - If there is a range separator without further verse declaration the last - refered chapter is addressed until the end. + If there is a range separator without further verse declaration the last refered chapter is addressed until the end. - ``range_regex`` is a regular expression which matches for verse range - declarations: + ``range_regex`` is a regular expression which matches for verse range declarations: ``(?:(?P[0-9]+)%(sep_v)s)?`` - It starts with a optional chapter reference ``from_chapter`` followed by - a verse separator. + It starts with a optional chapter reference ``from_chapter`` followed by a verse separator. ``(?P[0-9]+)`` The verse reference ``from_verse`` is manditory ``(?P%(sep_r)s(?:`` ... ``|%(sep_e)s)?)?`` - A ``range_to`` declaration is optional. It starts with a range separator - and contains optional a chapter and verse declaration or a end - separator. + A ``range_to`` declaration is optional. It starts with a range separator and contains optional a chapter and + verse declaration or a end separator. ``(?:(?P[0-9]+)%(sep_v)s)?`` The ``to_chapter`` reference with separator is equivalent to group 1. @@ -328,17 +317,15 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False): ``(?P[0-9]+)`` The ``to_verse`` reference is equivalent to group 2. - The full reference is matched against get_reference_match(u'full'). This - regular expression looks like this: + The full reference is matched against get_reference_match(u'full'). This regular expression looks like this: ``^\s*(?!\s)(?P[\d]*[^\d]+)(?(?:%(range_regex)s(?:%(sep_l)s(?!\s*$)|(?=\s*$)))+)\s*$`` - The second group contains all ``ranges``. This can be multiple - declarations of range_regex separated by a list separator. + The second group contains all ``ranges``. This can be multiple declarations of range_regex separated by a list + separator. """ log.debug(u'parse_reference("%s")', reference) diff --git a/openlp/plugins/bibles/lib/csvbible.py b/openlp/plugins/bibles/lib/csvbible.py index f57da2ba0..7cf004cfc 100644 --- a/openlp/plugins/bibles/lib/csvbible.py +++ b/openlp/plugins/bibles/lib/csvbible.py @@ -27,8 +27,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`cvsbible` modules provides a facility to import bibles from a set of -CSV files. +The :mod:`cvsbible` modules provides a facility to import bibles from a set of CSV files. The module expects two mandatory files containing the books and the verses. @@ -55,8 +54,7 @@ There are two acceptable formats of the verses file. They are: or "Genesis",1,2,"And the earth was without form, and void; and...." -All CSV files are expected to use a comma (',') as the delimiter and double -quotes ('"') as the quote symbol. +All CSV files are expected to use a comma (',') as the delimiter and double quotes ('"') as the quote symbol. """ import logging import chardet @@ -65,8 +63,10 @@ import csv from openlp.core.lib import translate from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB + log = logging.getLogger(__name__) + class CSVBible(BibleDB): """ This class provides a specialisation for importing of CSV Bibles. @@ -75,9 +75,8 @@ class CSVBible(BibleDB): def __init__(self, parent, **kwargs): """ - Loads a Bible from a set of CSV files. - This class assumes the files contain all the information and - a clean bible is being loaded. + Loads a Bible from a set of CSV files. This class assumes the files contain all the information and a clean + bible is being loaded. """ log.info(self.__class__.__name__) BibleDB.__init__(self, parent, **kwargs) diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index 4193668e2..837dbec38 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -511,7 +511,7 @@ class BibleDB(QtCore.QObject, Manager): language = None language_form = LanguageForm(self.wizard) if language_form.exec_(bible_name): - language = unicode(language_form.languageComboBox.currentText()) + language = unicode(language_form.language_combo_box.currentText()) if not language: return False language = BiblesResourcesDB.get_language(language) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index b01377a05..14c502265 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -27,8 +27,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`http` module enables OpenLP to retrieve scripture from bible -websites. +The :mod:`http` module enables OpenLP to retrieve scripture from bible websites. """ import logging import re diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index b5352db9f..b67bb99b2 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -61,8 +61,8 @@ class BibleMediaItem(MediaManagerItem): def __init__(self, parent, plugin): self.icon_path = u'songs/song' - self.lockIcon = build_icon(u':/bibles/bibles_search_lock.png') - self.unlockIcon = build_icon(u':/bibles/bibles_search_unlock.png') + self.lock_icon = build_icon(u':/bibles/bibles_search_lock.png') + self.unlock_icon = build_icon(u':/bibles/bibles_search_unlock.png') MediaManagerItem.__init__(self, parent, plugin) # Place to store the search results for both bibles. self.settings = self.plugin.settings_tab @@ -73,7 +73,7 @@ class BibleMediaItem(MediaManagerItem): self.check_search_result() Registry().register_function(u'bibles_load_list', self.reload_bibles) - def __checkSecondBible(self, bible, second_bible): + def __check_second_bible(self, bible, second_bible): """ Check if the first item is a second bible item or not. """ @@ -84,7 +84,7 @@ class BibleMediaItem(MediaManagerItem): self.displayResults(bible, second_bible) return else: - item_second_bible = self._decodeQtObject(bitem, 'second_bible') + item_second_bible = self._decode_qt_object(bitem, 'second_bible') if item_second_bible and second_bible or not item_second_bible and not second_bible: self.displayResults(bible, second_bible) elif critical_error_message_box( @@ -95,7 +95,7 @@ class BibleMediaItem(MediaManagerItem): self.list_view.clear() self.displayResults(bible, second_bible) - def _decodeQtObject(self, bitem, key): + def _decode_qt_object(self, bitem, key): reference = bitem.data(QtCore.Qt.UserRole) obj = reference[unicode(key)] return unicode(obj).strip() @@ -159,7 +159,7 @@ class BibleMediaItem(MediaManagerItem): search_button_layout.setObjectName(prefix + u'search_button_layout') search_button_layout.addStretch() lockButton = QtGui.QToolButton(tab) - lockButton.setIcon(self.unlockIcon) + lockButton.setIcon(self.unlock_icon) lockButton.setCheckable(True) lockButton.setObjectName(prefix + u'LockButton') search_button_layout.addWidget(lockButton) @@ -509,9 +509,9 @@ class BibleMediaItem(MediaManagerItem): def onLockButtonToggled(self, checked): if checked: - self.sender().setIcon(self.lockIcon) + self.sender().setIcon(self.lock_icon) else: - self.sender().setIcon(self.unlockIcon) + self.sender().setIcon(self.unlock_icon) def onQuickStyleComboBoxChanged(self): self.settings.layout_style = self.quickStyleComboBox.currentIndex() @@ -633,7 +633,7 @@ class BibleMediaItem(MediaManagerItem): if not self.advancedLockButton.isChecked(): self.list_view.clear() if self.list_view.count() != 0: - self.__checkSecondBible(bible, second_bible) + self.__check_second_bible(bible, second_bible) elif self.search_results: self.displayResults(bible, second_bible) self.advancedSearchButton.setEnabled(True) @@ -689,7 +689,7 @@ class BibleMediaItem(MediaManagerItem): if not self.quickLockButton.isChecked(): self.list_view.clear() if self.list_view.count() != 0 and self.search_results: - self.__checkSecondBible(bible, second_bible) + self.__check_second_bible(bible, second_bible) elif self.search_results: self.displayResults(bible, second_bible) self.quickSearchButton.setEnabled(True) @@ -787,19 +787,19 @@ class BibleMediaItem(MediaManagerItem): raw_title = [] verses = VerseReferenceList() for bitem in items: - book = self._decodeQtObject(bitem, 'book') - chapter = int(self._decodeQtObject(bitem, 'chapter')) - verse = int(self._decodeQtObject(bitem, 'verse')) - bible = self._decodeQtObject(bitem, 'bible') - version = self._decodeQtObject(bitem, 'version') - copyright = self._decodeQtObject(bitem, 'copyright') - permissions = self._decodeQtObject(bitem, 'permissions') - text = self._decodeQtObject(bitem, 'text') - second_bible = self._decodeQtObject(bitem, 'second_bible') - second_version = self._decodeQtObject(bitem, 'second_version') - second_copyright = self._decodeQtObject(bitem, 'second_copyright') - second_permissions = self._decodeQtObject(bitem, 'second_permissions') - second_text = self._decodeQtObject(bitem, 'second_text') + book = self._decode_qt_object(bitem, 'book') + chapter = int(self._decode_qt_object(bitem, 'chapter')) + verse = int(self._decode_qt_object(bitem, 'verse')) + bible = self._decode_qt_object(bitem, 'bible') + version = self._decode_qt_object(bitem, 'version') + copyright = self._decode_qt_object(bitem, 'copyright') + permissions = self._decode_qt_object(bitem, 'permissions') + text = self._decode_qt_object(bitem, 'text') + second_bible = self._decode_qt_object(bitem, 'second_bible') + second_version = self._decode_qt_object(bitem, 'second_version') + second_copyright = self._decode_qt_object(bitem, 'second_copyright') + second_permissions = self._decode_qt_object(bitem, 'second_permissions') + second_text = self._decode_qt_object(bitem, 'second_text') verses.add(book, chapter, verse, version, copyright, permissions) verse_text = self.formatVerse(old_chapter, chapter, verse) if second_bible: @@ -867,13 +867,13 @@ class BibleMediaItem(MediaManagerItem): """ verse_separator = get_reference_separator(u'sep_v_display') range_separator = get_reference_separator(u'sep_r_display') - old_chapter = self._decodeQtObject(old_bitem, 'chapter') - old_verse = self._decodeQtObject(old_bitem, 'verse') - start_book = self._decodeQtObject(start_bitem, 'book') - start_chapter = self._decodeQtObject(start_bitem, 'chapter') - start_verse = self._decodeQtObject(start_bitem, 'verse') - start_bible = self._decodeQtObject(start_bitem, 'bible') - start_second_bible = self._decodeQtObject(start_bitem, 'second_bible') + old_chapter = self._decode_qt_object(old_bitem, 'chapter') + old_verse = self._decode_qt_object(old_bitem, 'verse') + start_book = self._decode_qt_object(start_bitem, 'book') + start_chapter = self._decode_qt_object(start_bitem, 'chapter') + start_verse = self._decode_qt_object(start_bitem, 'verse') + start_bible = self._decode_qt_object(start_bitem, 'bible') + start_second_bible = self._decode_qt_object(start_bitem, 'second_bible') if start_second_bible: bibles = u'%s, %s' % (start_bible, start_second_bible) else: @@ -901,16 +901,16 @@ class BibleMediaItem(MediaManagerItem): The item we were previously dealing with. """ # Get all the necessary meta data. - book = self._decodeQtObject(bitem, 'book') - chapter = int(self._decodeQtObject(bitem, 'chapter')) - verse = int(self._decodeQtObject(bitem, 'verse')) - bible = self._decodeQtObject(bitem, 'bible') - second_bible = self._decodeQtObject(bitem, 'second_bible') - old_book = self._decodeQtObject(old_bitem, 'book') - old_chapter = int(self._decodeQtObject(old_bitem, 'chapter')) - old_verse = int(self._decodeQtObject(old_bitem, 'verse')) - old_bible = self._decodeQtObject(old_bitem, 'bible') - old_second_bible = self._decodeQtObject(old_bitem, 'second_bible') + book = self._decode_qt_object(bitem, 'book') + chapter = int(self._decode_qt_object(bitem, 'chapter')) + verse = int(self._decode_qt_object(bitem, 'verse')) + bible = self._decode_qt_object(bitem, 'bible') + second_bible = self._decode_qt_object(bitem, 'second_bible') + old_book = self._decode_qt_object(old_bitem, 'book') + old_chapter = int(self._decode_qt_object(old_bitem, 'chapter')) + old_verse = int(self._decode_qt_object(old_bitem, 'verse')) + old_bible = self._decode_qt_object(old_bitem, 'bible') + old_second_bible = self._decode_qt_object(old_bitem, 'second_bible') if old_bible != bible or old_second_bible != second_bible or old_book != book: # The bible, second bible or book has changed. return True diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index 9b4b216d7..9a6c62aad 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -34,16 +34,18 @@ from openlp.core.lib import translate from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB + log = logging.getLogger(__name__) + class OpenSongBible(BibleDB): """ OpenSong Bible format importer class. """ def __init__(self, parent, **kwargs): """ - Constructor to create and set up an instance of the OpenSongBible - class. This class is used to import Bibles from OpenSong's XML format. + Constructor to create and set up an instance of the OpenSongBible class. This class is used to import Bibles + from OpenSong's XML format. """ log.debug(self.__class__.__name__) BibleDB.__init__(self, parent, **kwargs) @@ -75,9 +77,8 @@ class OpenSongBible(BibleDB): file = None success = True try: - # NOTE: We don't need to do any of the normal encoding detection - # here, because lxml does it's own encoding detection, and the two - # mechanisms together interfere with each other. + # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding + # detection, and the two mechanisms together interfere with each other. file = open(self.filename, u'r') opensong = objectify.parse(file) bible = opensong.getroot() @@ -116,16 +117,11 @@ class OpenSongBible(BibleDB): if len(verse_parts) > 1: number = int(verse_parts[0]) except TypeError: - log.warn(u'Illegal verse number: %s', - unicode(verse.attrib[u'n'])) + log.warn(u'Illegal verse number: %s', unicode(verse.attrib[u'n'])) verse_number = number else: verse_number += 1 - self.create_verse( - db_book.id, - chapter_number, - verse_number, - self.get_text(verse)) + self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse)) self.wizard.increment_progress_bar(translate('BiblesPlugin.Opensong', 'Importing %s %s...', 'Importing ...')) % (db_book.name, chapter_number) self.session.commit() diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 981d10949..b91cd8319 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -39,9 +39,11 @@ from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB log = logging.getLogger(__name__) + def replacement(match): return match.group(2).upper() + class OSISBible(BibleDB): """ `OSIS `_ Bible format importer class. @@ -53,8 +55,7 @@ class OSISBible(BibleDB): BibleDB.__init__(self, parent, **kwargs) self.filename = kwargs[u'filename'] self.language_regex = re.compile(r'(.*?)') - self.verse_regex = re.compile( - r'(.*?)') + self.verse_regex = re.compile(r'(.*?)') self.note_regex = re.compile(r'(.*?)') self.title_regex = re.compile(r'(.*?)') self.milestone_regex = re.compile(r'') @@ -68,8 +69,7 @@ class OSISBible(BibleDB): self.q1_regex = re.compile(r'') self.q2_regex = re.compile(r'') self.trans_regex = re.compile(r'(.*?)') - self.divine_name_regex = re.compile( - r'(.*?)') + self.divine_name_regex = re.compile(r'(.*?)') self.spaces_regex = re.compile(r'([ ]{2,})') filepath = os.path.join( AppLocation.get_directory(AppLocation.PluginsDir), u'bibles', u'resources', u'osisbooks.csv') @@ -158,10 +158,8 @@ class OSISBible(BibleDB): self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Importing %s %s...', 'Importing ...') % (book_details[u'name'], chapter)) last_chapter = chapter - # All of this rigmarol below is because the mod2osis - # tool from the Sword library embeds XML in the OSIS - # but neglects to enclose the verse text (with XML) in - # <[CDATA[ ]]> tags. + # All of this rigmarol below is because the mod2osis tool from the Sword library embeds XML in the + # OSIS but neglects to enclose the verse text (with XML) in <[CDATA[ ]]> tags. verse_text = self.note_regex.sub(u'', verse_text) verse_text = self.title_regex.sub(u'', verse_text) verse_text = self.milestone_regex.sub(u'', verse_text) diff --git a/openlp/plugins/bibles/lib/upgrade.py b/openlp/plugins/bibles/lib/upgrade.py index b4a358c45..0c1a089b2 100644 --- a/openlp/plugins/bibles/lib/upgrade.py +++ b/openlp/plugins/bibles/lib/upgrade.py @@ -27,8 +27,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`upgrade` module provides a way for the database and schema that is the -backend for the Bibles plugin +The :mod:`upgrade` module provides a way for the database and schema that is the backend for the Bibles plugin. """ import logging diff --git a/openlp/plugins/bibles/lib/versereferencelist.py b/openlp/plugins/bibles/lib/versereferencelist.py index 1cb76ffff..069b86d5e 100644 --- a/openlp/plugins/bibles/lib/versereferencelist.py +++ b/openlp/plugins/bibles/lib/versereferencelist.py @@ -29,8 +29,8 @@ class VerseReferenceList(object): """ - The VerseReferenceList class encapsulates a list of verse references, but - maintains the order in which they were added. + The VerseReferenceList class encapsulates a list of verse references, but maintains the order in which they were + added. """ def __init__(self): From ae5869e3669bc6c7c488a30c38bd6bb062577758 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 18 Apr 2013 20:19:43 +0200 Subject: [PATCH 169/235] Add waiting cursor while searching. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index cf0f3922f..ee27d77a6 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -151,6 +151,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): # Hide back button. self.button(QtGui.QWizard.BackButton).hide() if page_id == self.searching_page_id: + self.application.set_busy_cursor() self.button(QtGui.QWizard.NextButton).hide() # Search duplicate songs. max_songs = self.plugin.manager.get_object_count(Song) @@ -177,6 +178,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.notify_no_duplicates() else: self.button(QtGui.QWizard.NextButton).show() + self.application.set_normal_cursor() elif page_id == self.review_page_id: self.process_current_duplicate_entry() From 25db18d34cb9ad6c1067297b1fc4e30610b647de Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 18 Apr 2013 22:15:44 +0200 Subject: [PATCH 170/235] Use inheritance instead of composition. Some interface cleanup. --- openlp/core/ui/listpreviewwidget.py | 113 +++++++++++++--------------- openlp/core/ui/servicemanager.py | 2 +- openlp/core/ui/slidecontroller.py | 11 ++- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index fb94555fd..e9025a255 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -35,72 +35,61 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import ImageSource, Registry, ServiceItem -class ListPreviewWidget(QtCore.QObject): - clicked = QtCore.pyqtSignal() - double_clicked = QtCore.pyqtSignal() - - def __init__(self, parent, is_live): - super(QtCore.QObject, self).__init__() - self.is_live = is_live - self.preview_table_widget = QtGui.QTableWidget(parent) - self.preview_table_widget.setColumnCount(1) - self.preview_table_widget.horizontalHeader().setVisible(False) - self.preview_table_widget.setColumnWidth(0, parent.width()) - self.preview_table_widget.setObjectName(u'preview_table_widget') - self.preview_table_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.preview_table_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) - self.preview_table_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) - self.preview_table_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.preview_table_widget.setAlternatingRowColors(True) +class ListPreviewWidget(QtGui.QTableWidget): + def __init__(self, parent, screen_ratio): + super(QtGui.QTableWidget, self).__init__(parent) self.service_item = ServiceItem() - if not self.is_live: - self.preview_table_widget.doubleClicked.connect(self._double_clicked) - self.preview_table_widget.clicked.connect(self._clicked) + self.screen_ratio = screen_ratio - def _clicked(self): - self.clicked.emit() + self.setColumnCount(1) + self.horizontalHeader().setVisible(False) + self.setColumnWidth(0, parent.width()) + self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setAlternatingRowColors(True) - def _double_clicked(self): - self.double_clicked.emit() + def resizeEvent(self, QResizeEvent): + self.__recalculate_layout() - def get_preview_widget(self): - return self.preview_table_widget - - def set_active(self, active): - if active: - self.preview_table_widget.show() - else: - self.preview_table_widget.hide() - - def preview_size_changed(self, width, ratio): - """ - Takes care of the SlidePreview's size. Is called when one of the the - splitters is moved or when the screen size is changed. Note, that this - method is (also) called frequently from the mainwindow *paintEvent*. - """ - self.preview_table_widget.setColumnWidth(0, self.preview_table_widget.viewport().size().width()) + def __recalculate_layout(self): + self.setColumnWidth(0, self.viewport().width()) if self.service_item: # Sort out songs, bibles, etc. if self.service_item.is_text(): - self.preview_table_widget.resizeRowsToContents() + self.resizeRowsToContents() else: # Sort out image heights. for framenumber in range(len(self.service_item.get_frames())): - self.preview_table_widget.setRowHeight(framenumber, width / ratio) + #self.setRowHeight(framenumber, width / ratio) + height = self.viewport().width() / self.screen_ratio + self.setRowHeight(framenumber, height) - def replace_service_manager_item(self, service_item, width, ratio, slideno): + #width = self.main_window.controlSplitter.sizes()[self.split] + def screen_size_changed(self, screen_ratio): + self.screen_ratio = screen_ratio + self.__recalculate_layout() + + def set_active(self, active): + if active: + self.show() + else: + self.hide() + + def replace_service_manager_item(self, service_item, width, slideno): """ Loads a ServiceItem into the system from ServiceManager Display the slide number passed """ self.service_item = service_item - self.preview_table_widget.clear() - self.preview_table_widget.setRowCount(0) - self.preview_table_widget.setColumnWidth(0, width) + self.clear() + self.setRowCount(0) + self.setColumnWidth(0, width) row = 0 text = [] for framenumber, frame in enumerate(self.service_item.get_frames()): - self.preview_table_widget.setRowCount(self.preview_table_widget.rowCount() + 1) + self.setRowCount(self.rowCount() + 1) item = QtGui.QTableWidgetItem() slideHeight = 0 if self.service_item.is_text(): @@ -129,26 +118,26 @@ class ListPreviewWidget(QtCore.QObject): ImageSource.ImagePlugin) image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin) label.setPixmap(QtGui.QPixmap.fromImage(image)) - self.preview_table_widget.setCellWidget(framenumber, 0, label) - slideHeight = width / ratio + self.setCellWidget(framenumber, 0, label) + slideHeight = width / self.screen_ratio row += 1 text.append(unicode(row)) - self.preview_table_widget.setItem(framenumber, 0, item) + self.setItem(framenumber, 0, item) if slideHeight: - self.preview_table_widget.setRowHeight(framenumber, slideHeight) - self.preview_table_widget.setVerticalHeaderLabels(text) + self.setRowHeight(framenumber, slideHeight) + self.setVerticalHeaderLabels(text) if self.service_item.is_text(): - self.preview_table_widget.resizeRowsToContents() - self.preview_table_widget.setColumnWidth(0, self.preview_table_widget.viewport().size().width()) + self.resizeRowsToContents() + self.setColumnWidth(0, self.viewport().width()) #stuff happens here, perhaps the setFocus() has to happen later... - self.preview_table_widget.setFocus() + self.setFocus() def update_preview_selection(self, row): """ Utility method to update the selected slide in the list. """ - if row >= self.preview_table_widget.rowCount(): - self.preview_table_widget.selectRow(self.preview_table_widget.rowCount() - 1) + if row >= self.rowCount(): + self.selectRow(self.rowCount() - 1) else: self.check_update_selected_slide(row) @@ -156,15 +145,15 @@ class ListPreviewWidget(QtCore.QObject): """ Check if this slide has been updated """ - if row + 1 < self.preview_table_widget.rowCount(): - self.preview_table_widget.scrollToItem(self.preview_table_widget.item(row + 1, 0)) - self.preview_table_widget.selectRow(row) + if row + 1 < self.rowCount(): + self.scrollToItem(self.item(row + 1, 0)) + self.selectRow(row) def currentRow(self): - return self.preview_table_widget.currentRow() + return super(ListPreviewWidget, self).currentRow() def rowCount(self): - return self.preview_table_widget.rowCount() + return super(ListPreviewWidget, self).rowCount() def _get_image_manager(self): """ diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 4aa7c5307..51e3f790d 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1379,7 +1379,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0) next_item = self.service_manager_list.topLevelItem(item) self.service_manager_list.setCurrentItem(next_item) - self.live_controller.preview_widget.get_preview_widget().setFocus() + self.live_controller.preview_widget.setFocus() else: critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'), translate('OpenLP.ServiceManager', diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index d1694ca56..6e5e5c8b7 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -158,8 +158,8 @@ class SlideController(DisplayController): self.controller_layout.setSpacing(0) self.controller_layout.setMargin(0) # Controller list view - self.preview_widget = ListPreviewWidget(self, self.is_live) - self.controller_layout.addWidget(self.preview_widget.get_preview_widget()) + self.preview_widget = ListPreviewWidget(self, self.ratio) + self.controller_layout.addWidget(self.preview_widget) # Build the full toolbar self.toolbar = OpenLPToolbar(self) size_toolbar_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) @@ -356,7 +356,7 @@ class SlideController(DisplayController): self.toolbar.set_widget_visible(self.loop_list, False) self.toolbar.set_widget_visible(self.wide_menu, False) else: - self.preview_widget.double_clicked.connect(self.onGoLiveClick) + self.preview_widget.doubleClicked.connect(self.onGoLiveClick) self.toolbar.set_widget_visible([u'editSong'], False) if self.is_live: self.setLiveHotkeys(self) @@ -524,6 +524,7 @@ class SlideController(DisplayController): self.ratio = 1 self.media_controller.setup_display(self.display, False) self.previewSizeChanged() + self.preview_widget.screen_size_changed(self.ratio) self.preview_display.setup() service_item = ServiceItem() self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live, @@ -562,8 +563,6 @@ class SlideController(DisplayController): self.preview_display.screen = { u'size': self.preview_display.geometry()} # Make sure that the frames have the correct size. - width = self.main_window.controlSplitter.sizes()[self.split] - self.preview_widget.preview_size_changed(width, self.ratio) self.onControllerSizeChanged(self.controller.width()) def onControllerSizeChanged(self, width): @@ -777,7 +776,7 @@ class SlideController(DisplayController): slideHeight = width * (1 / self.ratio) row += 1 self.slideList[unicode(row)] = row - 1 - self.preview_widget.replace_service_manager_item(self.service_item, width, self.ratio, slideno) + self.preview_widget.replace_service_manager_item(self.service_item, width, slideno) self.preview_widget.update_preview_selection(slideno) self.enableToolBar(service_item) # Pass to display for viewing. From 2b9a13ff97fdd84ad63ade409e2333d7801f1d0f Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 18 Apr 2013 22:50:02 +0200 Subject: [PATCH 171/235] Clean up PreviewWidget interface a little more. --- openlp/core/ui/listpreviewwidget.py | 52 +++++++++++++---------------- openlp/core/ui/slidecontroller.py | 23 +++++++------ 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index e9025a255..78b9d26e4 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -51,9 +51,16 @@ class ListPreviewWidget(QtGui.QTableWidget): self.setAlternatingRowColors(True) def resizeEvent(self, QResizeEvent): + """ + Overloaded method from QTableWidget. Will recalculate the layout. + """ self.__recalculate_layout() def __recalculate_layout(self): + """ + Recalculates the layout of the table widget. It will set height and width + of the table cells. QTableWidget does not adapt the cells to the widget size at all. + """ self.setColumnWidth(0, self.viewport().width()) if self.service_item: # Sort out songs, bibles, etc. @@ -66,21 +73,18 @@ class ListPreviewWidget(QtGui.QTableWidget): height = self.viewport().width() / self.screen_ratio self.setRowHeight(framenumber, height) - #width = self.main_window.controlSplitter.sizes()[self.split] def screen_size_changed(self, screen_ratio): + """ + To be called whenever the live screen size changes. + Because this makes a layout recalculation necessary. + """ self.screen_ratio = screen_ratio self.__recalculate_layout() - def set_active(self, active): - if active: - self.show() - else: - self.hide() - - def replace_service_manager_item(self, service_item, width, slideno): + def replace_service_manager_item(self, service_item, width, slide): """ - Loads a ServiceItem into the system from ServiceManager - Display the slide number passed + Replaces the current preview items with the ones in service_item. + Displays the given slide. """ self.service_item = service_item self.clear() @@ -112,10 +116,6 @@ class ListPreviewWidget(QtGui.QTableWidget): if self.service_item.is_command(): label.setPixmap(QtGui.QPixmap(frame[u'image'])) else: - # If current slide set background to image - if framenumber == slideno: - self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'], - ImageSource.ImagePlugin) image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin) label.setPixmap(QtGui.QPixmap.fromImage(image)) self.setCellWidget(framenumber, 0, label) @@ -129,25 +129,19 @@ class ListPreviewWidget(QtGui.QTableWidget): if self.service_item.is_text(): self.resizeRowsToContents() self.setColumnWidth(0, self.viewport().width()) - #stuff happens here, perhaps the setFocus() has to happen later... self.setFocus() + self.change_slide(slide) - def update_preview_selection(self, row): + def change_slide(self, slide): """ - Utility method to update the selected slide in the list. + Switches to the given row. """ - if row >= self.rowCount(): - self.selectRow(self.rowCount() - 1) - else: - self.check_update_selected_slide(row) - - def check_update_selected_slide(self, row): - """ - Check if this slide has been updated - """ - if row + 1 < self.rowCount(): - self.scrollToItem(self.item(row + 1, 0)) - self.selectRow(row) + if slide >= self.rowCount(): + slide = self.rowCount() - 1 + #Scroll to next item if possible. + if slide + 1 < self.rowCount(): + self.scrollToItem(self.item(slide + 1, 0)) + self.selectRow(slide) def currentRow(self): return super(ListPreviewWidget, self).currentRow() diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 6e5e5c8b7..3ae696277 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -36,7 +36,7 @@ from collections import deque from PyQt4 import QtCore, QtGui -from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, SlideLimits, \ +from openlp.core.lib import OpenLPToolbar, ImageSource, ItemCapabilities, ServiceItem, SlideLimits, \ ServiceItemAction, Settings, Registry, UiStrings, ScreenList, build_icon, build_html, translate from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType from openlp.core.lib.ui import create_action @@ -418,7 +418,7 @@ class SlideController(DisplayController): if len(matches) == 1: self.shortcutTimer.stop() self.current_shortcut = u'' - self.preview_widget.check_update_selected_slide(self.slideList[matches[0]]) + self.preview_widget.change_slide(self.slideList[matches[0]]) self.slideSelected() elif sender_name != u'shortcutTimer': # Start the time as we did not have any match. @@ -428,7 +428,7 @@ class SlideController(DisplayController): if self.current_shortcut in keys: # We had more than one match for example "V1" and "V10", but # "V1" was the slide we wanted to go. - self.preview_widget.check_update_selected_slide(self.slideList[self.current_shortcut]) + self.preview_widget.change_slide(self.slideList[self.current_shortcut]) self.slideSelected() # Reset the shortcut. self.current_shortcut = u'' @@ -704,7 +704,7 @@ class SlideController(DisplayController): slidenum = 0 # If service item is the same as the current one, only change slide if slideno >= 0 and item == self.service_item: - self.preview_widget.check_update_selected_slide(slidenum) + self.preview_widget.change_slide(slidenum) self.slideSelected() else: self._processItem(item, slidenum) @@ -776,8 +776,11 @@ class SlideController(DisplayController): slideHeight = width * (1 / self.ratio) row += 1 self.slideList[unicode(row)] = row - 1 + # If current slide set background to image + if not self.service_item.is_command() and framenumber == slideno: + self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'], + ImageSource.ImagePlugin) self.preview_widget.replace_service_manager_item(self.service_item, width, slideno) - self.preview_widget.update_preview_selection(slideno) self.enableToolBar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme @@ -810,7 +813,7 @@ class SlideController(DisplayController): Registry().execute(u'%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) self.updatePreview() else: - self.preview_widget.check_update_selected_slide(index) + self.preview_widget.change_slide(index) self.slideSelected() def mainDisplaySetBackground(self): @@ -973,7 +976,7 @@ class SlideController(DisplayController): self.service_item.bg_image_bytes = None self.updatePreview() self.selected_row = row - self.preview_widget.check_update_selected_slide(row) + self.preview_widget.change_slide(row) Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) self.display.setFocus() @@ -981,7 +984,7 @@ class SlideController(DisplayController): """ The slide has been changed. Update the slidecontroller accordingly """ - self.preview_widget.check_update_selected_slide(row) + self.preview_widget.change_slide(row) self.updatePreview() Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) @@ -1040,7 +1043,7 @@ class SlideController(DisplayController): row = 0 else: row = self.preview_widget.rowCount() - 1 - self.preview_widget.check_update_selected_slide(row) + self.preview_widget.change_slide(row) self.slideSelected() def on_slide_selected_previous(self): @@ -1063,7 +1066,7 @@ class SlideController(DisplayController): return else: row = 0 - self.preview_widget.check_update_selected_slide(row) + self.preview_widget.change_slide(row) self.slideSelected() def onToggleLoop(self): From dad243dd743835acb79c80e2f5a753406b916a2e Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 19 Apr 2013 20:52:39 +0200 Subject: [PATCH 172/235] 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 173/235] 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 174/235] 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 175/235] 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 176/235] 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 177/235] 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 178/235] 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 e486bac91c0082385e5466ae4b65168566fb045b Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 20 Apr 2013 19:36:18 +0200 Subject: [PATCH 179/235] removed entry from combo box --- openlp/plugins/bibles/forms/bibleimportform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 242dff707..f617ffecd 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -128,7 +128,7 @@ class BibleImportForm(OpenLPWizard): self.formatLabel = QtGui.QLabel(self.selectPage) self.formatLabel.setObjectName(u'FormatLabel') self.formatComboBox = QtGui.QComboBox(self.selectPage) - self.formatComboBox.addItems([u'', u'', u'', u'', u'']) + self.formatComboBox.addItems([u'', u'', u'', u'']) self.formatComboBox.setObjectName(u'FormatComboBox') self.formatLayout.addRow(self.formatLabel, self.formatComboBox) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) From 401d4d268875b78858114e2c9165149ad15a29f4 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 20 Apr 2013 20:39:10 +0200 Subject: [PATCH 180/235] 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 From 34180112be19d5fd4202a5558a6a0d79e8584617 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 21 Apr 2013 08:06:25 +0100 Subject: [PATCH 181/235] Fix Core from abend --- 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 d4ee8039c..60393dfbb 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -185,7 +185,7 @@ def check_latest_version(current_version): version_string = current_version[u'full'] # set to prod in the distribution config file. settings = Settings() - settings.beginGroup(u'general') + settings.beginGroup(u'core') last_test = settings.value(u'last version test') this_test = unicode(datetime.now().date()) settings.setValue(u'last version test', this_test) From 02b1f7b78b72b333a665095e835fa82f920d6c4d Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 21 Apr 2013 08:21:29 +0100 Subject: [PATCH 182/235] and more --- 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 35527e1e4..f219cea45 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -255,12 +255,12 @@ class SlideController(DisplayController): self.delay_spin_box.setToolTip(translate('OpenLP.SlideController', 'Delay between slides in seconds.')) self.toolbar.add_toolbar_widget(self.delay_spin_box) else: - self.toolbar.add_toolbar_action(u'goLive', icon=u':/general/general_live.png', + self.toolbar.add_toolbar_action(u'goLive', icon=u':/core/general_live.png', tooltip=translate('OpenLP.SlideController', 'Move to live.'), triggers=self.onGoLive) - self.toolbar.add_toolbar_action(u'addToService', icon=u':/general/general_add.png', + self.toolbar.add_toolbar_action(u'addToService', icon=u':/core/general_add.png', tooltip=translate('OpenLP.SlideController', 'Add to Service.'), triggers=self.onPreviewAddToService) self.toolbar.addSeparator() - self.toolbar.add_toolbar_action(u'editSong', icon=u':/general/general_edit.png', + self.toolbar.add_toolbar_action(u'editSong', icon=u':/core/general_edit.png', tooltip=translate('OpenLP.SlideController', 'Edit and reload song preview.'), triggers=self.onEditSong) self.controller_layout.addWidget(self.toolbar) # Build the Media Toolbar @@ -616,7 +616,7 @@ class SlideController(DisplayController): """ Adjusts the value of the ``delay_spin_box`` to the given one. """ - self.delay_spin_box.setValue(Settings().value(u'general/loop delay')) + self.delay_spin_box.setValue(Settings().value(u'core/loop delay')) def update_slide_limits(self): """ From 2965271adad4106e0c8ada632c77d4b3d8e5bc13 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 21 Apr 2013 08:26:45 +0100 Subject: [PATCH 183/235] and more --- openlp/core/ui/slidecontroller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index f219cea45..349ad96df 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -255,12 +255,12 @@ class SlideController(DisplayController): self.delay_spin_box.setToolTip(translate('OpenLP.SlideController', 'Delay between slides in seconds.')) self.toolbar.add_toolbar_widget(self.delay_spin_box) else: - self.toolbar.add_toolbar_action(u'goLive', icon=u':/core/general_live.png', + self.toolbar.add_toolbar_action(u'goLive', icon=u':/general/general_live.png', tooltip=translate('OpenLP.SlideController', 'Move to live.'), triggers=self.onGoLive) - self.toolbar.add_toolbar_action(u'addToService', icon=u':/core/general_add.png', + self.toolbar.add_toolbar_action(u'addToService', icon=u':/general/general_add.png', tooltip=translate('OpenLP.SlideController', 'Add to Service.'), triggers=self.onPreviewAddToService) self.toolbar.addSeparator() - self.toolbar.add_toolbar_action(u'editSong', icon=u':/core/general_edit.png', + self.toolbar.add_toolbar_action(u'editSong', icon=u':/general/general_edit.png', tooltip=translate('OpenLP.SlideController', 'Edit and reload song preview.'), triggers=self.onEditSong) self.controller_layout.addWidget(self.toolbar) # Build the Media Toolbar From 581c83e70ddb9c7e4531cc2442cc9d134fec18e4 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 21 Apr 2013 16:53:51 +0100 Subject: [PATCH 184/235] Final tidyups --- openlp/core/lib/mediamanageritem.py | 8 ++-- openlp/core/ui/servicemanager.py | 17 ++------- openlp/plugins/custom/lib/mediaitem.py | 7 ++-- openlp/plugins/songs/lib/mediaitem.py | 53 ++++++++++++++++---------- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 01329b842..331a9033a 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -102,7 +102,6 @@ class MediaManagerItem(QtGui.QWidget): self.setupUi() self.retranslateUi() self.auto_select_id = -1 - Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load) # Need to use event as called across threads and UI is updated QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote) QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote) @@ -585,12 +584,15 @@ class MediaManagerItem(QtGui.QWidget): else: return None - def service_load(self, message): + def service_load(self, item): """ Method to add processing when a service has been loaded and individual service items need to be processed by the plugins. + + ``item`` + The item to be processed and returned. """ - pass + return item def check_search_result(self): """ diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index f12d55207..5674554e2 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -715,13 +715,10 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): else: service_item.set_from_service(item, self.servicePath) service_item.validate_item(self.suffixes) - self.load_item_unique_identifier = 0 if service_item.is_capable(ItemCapabilities.OnLoadUpdate): - Registry().execute(u'%s_service_load' % service_item.name.lower(), service_item) - # if the item has been processed - if service_item.unique_identifier == self.load_item_unique_identifier: - service_item.edit_id = int(self.load_item_edit_id) - service_item.temporary_edit = self.load_item_temporary + new_item = Registry().get(service_item.name).service_load(service_item) + if new_item: + service_item = new_item self.add_service_item(service_item, repaint=False) delete_file(p_file) self.main_window.add_recent_file(file_name) @@ -1260,14 +1257,6 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.repaint_service_list(-1, -1) self.application.set_normal_cursor() - def service_item_update(self, edit_id, unique_identifier, temporary=False): - """ - Triggered from plugins to update service items. Save the values as they will be used as part of the service load - """ - self.load_item_unique_identifier = unique_identifier - self.load_item_edit_id = int(edit_id) - self.load_item_temporary = str_to_bool(temporary) - def replace_service_item(self, newItem): """ Using the service item passed replace the one with the same edit id if found. diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 071f68839..657e138df 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -40,6 +40,7 @@ from openlp.plugins.custom.lib.db import CustomSlide log = logging.getLogger(__name__) + class CustomSearch(object): """ An enumeration for custom search methods. @@ -216,7 +217,6 @@ class CustomMediaItem(MediaManagerItem): Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_keywords = self.search_text_edit.displayText() - search_results = [] search_type = self.search_text_edit.current_search_type() if search_type == CustomSearch.Titles: log.debug(u'Titles Search') @@ -255,7 +255,8 @@ class CustomMediaItem(MediaManagerItem): and_(CustomSlide.title == item.title, CustomSlide.theme_name == item.theme, CustomSlide.credits == item.raw_footer[0][len(item.title) + 1:])) if custom: - self.service_manager.service_item_update(custom.id, item.unique_identifier) + item.edit_id = custom.id + return item else: if self.add_custom_from_service: self.create_from_service_item(item) @@ -284,8 +285,6 @@ class CustomMediaItem(MediaManagerItem): custom.text = unicode(custom_xml.extract_xml(), u'utf-8') self.plugin.manager.save_object(custom) self.on_search_text_button_clicked() - if item.name.lower() == u'custom': - Registry().execute(u'service_item_update', u'%s:%s:%s' % (custom.id, item.unique_identifier, False)) def onClearTextButtonClick(self): """ diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 1565169e2..82ec1086c 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -160,7 +160,6 @@ class SongMediaItem(MediaManagerItem): Settings().setValue(u'%s/last search type' % self.settings_section, self.search_text_edit.current_search_type()) # Reload the list considering the new search type. search_keywords = unicode(self.search_text_edit.displayText()) - search_results = [] search_type = self.search_text_edit.current_search_type() if search_type == SongSearch.Entire: log.debug(u'Entire Song Search') @@ -463,16 +462,7 @@ class SongMediaItem(MediaManagerItem): for slide in verses: service_item.add_from_text(unicode(slide)) service_item.title = song.title - author_list = [unicode(author.display_name) for author in song.authors] - 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'core/ccli number'): - service_item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + - Settings().value(u'core/ccli number')) - service_item.audit = [ - song.title, author_list, song.copyright, unicode(song.ccli_number) - ] + author_list = self.generate_footer(service_item, song) service_item.data_string = {u'title': song.search_title, u'authors': u', '.join(author_list)} service_item.xml_version = self.openLyrics.song_to_xml(song) # Add the audio file to the service item. @@ -481,6 +471,30 @@ class SongMediaItem(MediaManagerItem): service_item.background_audio = [m.file_name for m in song.media_files] return True + def generate_footer(self, item, song): + """ + Generates the song footer based on a song and adds details to a service item. + author_list is only required for initial song generation. + + ``item`` + The service item to be amended + + ``song`` + The song to be used to generate the footer + """ + author_list = [unicode(author.display_name) for author in song.authors] + item.audit = [ + song.title, author_list, song.copyright, unicode(song.ccli_number) + ] + item.raw_footer = [] + item.raw_footer.append(song.title) + item.raw_footer.append(create_separated_list(author_list)) + item.raw_footer.append(song.copyright) + if Settings().value(u'core/ccli number'): + item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + + Settings().value(u'core/ccli number')) + return author_list + def service_load(self, item): """ Triggered by a song being loaded by the service manager. @@ -499,9 +513,8 @@ class SongMediaItem(MediaManagerItem): else: search_results = self.plugin.manager.get_all_objects(Song, Song.search_title == item.data_string[u'title'], Song.search_title.asc()) - editId = 0 + edit_id = 0 add_song = True - temporary = False if search_results: for song in search_results: author_list = item.data_string[u'authors'] @@ -514,7 +527,7 @@ class SongMediaItem(MediaManagerItem): break if same_authors and author_list.strip(u', ') == u'': add_song = False - editId = song.id + edit_id = song.id break # If there's any backing tracks, copy them over. if item.background_audio: @@ -524,7 +537,7 @@ class SongMediaItem(MediaManagerItem): # If there's any backing tracks, copy them over. if item.background_audio: self._updateBackgroundAudio(song, item) - editId = song.id + edit_id = song.id self.on_search_text_button_clicked() elif add_song and not self.addSongFromService: # Make sure we temporary import formatting tags. @@ -532,11 +545,11 @@ class SongMediaItem(MediaManagerItem): # If there's any backing tracks, copy them over. if item.background_audio: self._updateBackgroundAudio(song, item) - editId = song.id - temporary = True - # Update service with correct song id. - if editId: - self.service_manager.service_item_update(editId, item.unique_identifier, temporary) + edit_id = song.id + # Update service with correct song id and return it to caller. + item.edit_id = edit_id + self.generate_footer(item, song) + return item def search(self, string, showError): """ From 823e7b1ec0163385e7b2ba6cdf094bdfaa624a21 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Mon, 22 Apr 2013 22:51:33 +0200 Subject: [PATCH 185/235] Change a line back to trunk state. --- openlp/core/ui/slidecontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 18d2eed09..36a47ae87 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -36,7 +36,7 @@ from collections import deque from PyQt4 import QtCore, QtGui -from openlp.core.lib import OpenLPToolbar, ImageSource, ItemCapabilities, ServiceItem, SlideLimits, \ +from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, SlideLimits, \ ServiceItemAction, Settings, Registry, UiStrings, ScreenList, build_icon, build_html, translate from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType from openlp.core.lib.ui import create_action From 145a1aedcb6f9bb6022428e66636f5677985243a Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 23 Apr 2013 19:12:08 +0200 Subject: [PATCH 186/235] Add some docstrings and rename some methods for clarity. --- openlp/core/ui/listpreviewwidget.py | 36 +++++++++++++++++++---------- openlp/core/ui/slidecontroller.py | 24 +++++++++---------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 78b9d26e4..636d58468 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -27,7 +27,8 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller +The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller. +It is based on a QTableWidget but represents its contents in list form. """ from PyQt4 import QtCore, QtGui @@ -37,6 +38,11 @@ from openlp.core.lib import ImageSource, Registry, ServiceItem class ListPreviewWidget(QtGui.QTableWidget): def __init__(self, parent, screen_ratio): + """ + Initializes the widget to default state. + An empty ServiceItem is used per default. + One needs to call replace_service_manager_item() to make this widget display something. + """ super(QtGui.QTableWidget, self).__init__(parent) self.service_item = ServiceItem() self.screen_ratio = screen_ratio @@ -59,7 +65,7 @@ class ListPreviewWidget(QtGui.QTableWidget): def __recalculate_layout(self): """ Recalculates the layout of the table widget. It will set height and width - of the table cells. QTableWidget does not adapt the cells to the widget size at all. + of the table cells. QTableWidget does not adapt the cells to the widget size on its own. """ self.setColumnWidth(0, self.viewport().width()) if self.service_item: @@ -81,7 +87,7 @@ class ListPreviewWidget(QtGui.QTableWidget): self.screen_ratio = screen_ratio self.__recalculate_layout() - def replace_service_manager_item(self, service_item, width, slide): + def replace_service_manager_item(self, service_item, width, slideNumber): """ Replaces the current preview items with the ones in service_item. Displays the given slide. @@ -93,7 +99,7 @@ class ListPreviewWidget(QtGui.QTableWidget): row = 0 text = [] for framenumber, frame in enumerate(self.service_item.get_frames()): - self.setRowCount(self.rowCount() + 1) + self.setRowCount(self.slide_count() + 1) item = QtGui.QTableWidgetItem() slideHeight = 0 if self.service_item.is_text(): @@ -130,28 +136,34 @@ class ListPreviewWidget(QtGui.QTableWidget): self.resizeRowsToContents() self.setColumnWidth(0, self.viewport().width()) self.setFocus() - self.change_slide(slide) + self.change_slide(slideNumber) def change_slide(self, slide): """ Switches to the given row. """ - if slide >= self.rowCount(): - slide = self.rowCount() - 1 + if slide >= self.slide_count(): + slide = self.slide_count() - 1 #Scroll to next item if possible. - if slide + 1 < self.rowCount(): + if slide + 1 < self.slide_count(): self.scrollToItem(self.item(slide + 1, 0)) self.selectRow(slide) - def currentRow(self): + def current_slide_number(self): + """ + Returns the position of the currently active item. Will return -1 if the widget is empty. + """ return super(ListPreviewWidget, self).currentRow() - def rowCount(self): + def slide_count(self): + """ + Returns the number of slides this widget holds. + """ return super(ListPreviewWidget, self).rowCount() def _get_image_manager(self): """ - Adds the image manager to the class dynamically + Adds the image manager to the class dynamically. """ if not hasattr(self, u'_image_manager'): self._image_manager = Registry().get(u'image_manager') @@ -161,7 +173,7 @@ class ListPreviewWidget(QtGui.QTableWidget): def _get_main_window(self): """ - Adds the main window to the class dynamically + Adds the main window to the class dynamically. """ if not hasattr(self, u'_main_window'): self._main_window = Registry().get(u'main_window') diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 76616d152..2c24faca0 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -692,7 +692,7 @@ class SlideController(DisplayController): Replacement item following a remote edit """ if item == self.service_item: - self._processItem(item, self.preview_widget.currentRow()) + self._processItem(item, self.preview_widget.current_slide_number()) def addServiceManagerItem(self, item, slideno): """ @@ -960,9 +960,9 @@ class SlideController(DisplayController): Generate the preview when you click on a slide. if this is the Live Controller also display on the screen """ - row = self.preview_widget.currentRow() + row = self.preview_widget.current_slide_number() self.selected_row = 0 - if -1 < row < self.preview_widget.rowCount(): + if -1 < row < self.preview_widget.slide_count(): if self.service_item.is_command(): if self.is_live and not start: Registry().execute(u'%s_slide' % self.service_item.name.lower(), @@ -1033,8 +1033,8 @@ class SlideController(DisplayController): if self.service_item.is_command() and self.is_live: self.updatePreview() else: - row = self.preview_widget.currentRow() + 1 - if row == self.preview_widget.rowCount(): + row = self.preview_widget.current_slide_number() + 1 + if row == self.preview_widget.slide_count(): if wrap is None: if self.slide_limits == SlideLimits.Wrap: row = 0 @@ -1042,11 +1042,11 @@ class SlideController(DisplayController): self.serviceNext() return else: - row = self.preview_widget.rowCount() - 1 + row = self.preview_widget.slide_count() - 1 elif wrap: row = 0 else: - row = self.preview_widget.rowCount() - 1 + row = self.preview_widget.slide_count() - 1 self.preview_widget.change_slide(row) self.slideSelected() @@ -1060,10 +1060,10 @@ class SlideController(DisplayController): if self.service_item.is_command() and self.is_live: self.updatePreview() else: - row = self.preview_widget.currentRow() - 1 + row = self.preview_widget.current_slide_number() - 1 if row == -1: if self.slide_limits == SlideLimits.Wrap: - row = self.preview_widget.rowCount() - 1 + row = self.preview_widget.slide_count() - 1 elif self.is_live and self.slide_limits == SlideLimits.Next: self.keypress_queue.append(ServiceItemAction.PreviousLastSlide) self._process_queue() @@ -1087,7 +1087,7 @@ class SlideController(DisplayController): """ Start the timer loop running and store the timer id """ - if self.preview_widget.rowCount() > 1: + if self.preview_widget.slide_count() > 1: self.timer_id = self.startTimer(int(self.delay_spin_box.value()) * 1000) def on_stop_loop(self): @@ -1197,8 +1197,8 @@ class SlideController(DisplayController): """ If preview copy slide item to live controller from Preview Controller """ - row = self.preview_widget.currentRow() - if -1 < row < self.preview_widget.rowCount(): + row = self.preview_widget.current_slide_number() + if -1 < row < self.preview_widget.slide_count(): if self.service_item.from_service: self.service_manager.preview_live(self.service_item.unique_identifier, row) else: From edb0eab31d9aa0e64bdf5357b0c943d52b92aa28 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 23 Apr 2013 19:14:35 +0200 Subject: [PATCH 187/235] Remove a dev comment. Pretify a comment. --- openlp/core/ui/listpreviewwidget.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 636d58468..d259d43e7 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -75,7 +75,6 @@ class ListPreviewWidget(QtGui.QTableWidget): else: # Sort out image heights. for framenumber in range(len(self.service_item.get_frames())): - #self.setRowHeight(framenumber, width / ratio) height = self.viewport().width() / self.screen_ratio self.setRowHeight(framenumber, height) @@ -144,7 +143,7 @@ class ListPreviewWidget(QtGui.QTableWidget): """ if slide >= self.slide_count(): slide = self.slide_count() - 1 - #Scroll to next item if possible. + # Scroll to next item if possible. if slide + 1 < self.slide_count(): self.scrollToItem(self.item(slide + 1, 0)) self.selectRow(slide) From fba86c5552749c09f3b483f6011908ed946db597 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 23 Apr 2013 20:08:07 +0100 Subject: [PATCH 188/235] Add tests --- openlp/plugins/songs/forms/__init__.py | 1 + openlp/plugins/songs/lib/mediaitem.py | 9 +- openlp/plugins/songs/songsplugin.py | 3 +- .../functional/openlp_core_lib/test_screen.py | 2 +- .../openlp_plugins/songs/test_mediaitem.py | 125 ++++++++++++++++++ 5 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 tests/functional/openlp_plugins/songs/test_mediaitem.py diff --git a/openlp/plugins/songs/forms/__init__.py b/openlp/plugins/songs/forms/__init__.py index a1c177a9d..d6aef47dc 100644 --- a/openlp/plugins/songs/forms/__init__.py +++ b/openlp/plugins/songs/forms/__init__.py @@ -52,3 +52,4 @@ 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 editsongform import EditSongForm \ No newline at end of file diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 82ec1086c..f1ae52d07 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -72,10 +72,7 @@ class SongMediaItem(MediaManagerItem): def __init__(self, parent, plugin): self.icon_path = u'songs/song' MediaManagerItem.__init__(self, parent, plugin) - self.editSongForm = EditSongForm(self, self.main_window, self.plugin.manager) - self.openLyrics = OpenLyrics(self.plugin.manager) self.single_service_item = False - self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) # Holds information about whether the edit is remotely triggered and # which Song is required. self.remoteSong = -1 @@ -135,6 +132,12 @@ class SongMediaItem(MediaManagerItem): 'Maintain the lists of authors, topics and books.')) def initialise(self): + """ + Initialise variables when they cannot be initialised in the constructor. + """ + self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) + self.editSongForm = EditSongForm(self, self.main_window, self.plugin.manager) + self.openLyrics = OpenLyrics(self.plugin.manager) self.search_text_edit.set_search_types([ (SongSearch.Entire, u':/songs/song_search_all.png', translate('SongsPlugin.MediaItem', 'Entire Song'), diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 65056ee5b..4355bc7e5 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -235,8 +235,7 @@ class SongsPlugin(Plugin): u'delete': translate('SongsPlugin', 'Delete the selected song.'), u'preview': translate('SongsPlugin', 'Preview the selected song.'), u'live': translate('SongsPlugin', 'Send the selected song live.'), - u'service': translate('SongsPlugin', - 'Add the selected song to the service.') + u'service': translate('SongsPlugin', 'Add the selected song to the service.') } self.set_plugin_ui_text_strings(tooltips) diff --git a/tests/functional/openlp_core_lib/test_screen.py b/tests/functional/openlp_core_lib/test_screen.py index 007889a12..7c1bcaf24 100644 --- a/tests/functional/openlp_core_lib/test_screen.py +++ b/tests/functional/openlp_core_lib/test_screen.py @@ -1,7 +1,7 @@ """ Package to test the openlp.core.lib.screenlist package. """ -import copy + from unittest import TestCase from mock import MagicMock diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py new file mode 100644 index 000000000..5f5c161eb --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -0,0 +1,125 @@ +""" +This module contains tests for the lib submodule of the Songs plugin. +""" +import os +from tempfile import mkstemp +from unittest import TestCase + +from mock import patch, MagicMock + +from PyQt4 import QtGui + +from openlp.core.lib import Registry, ServiceItem, Settings + +from openlp.plugins.songs.lib.mediaitem import SongMediaItem + + +class TestMediaItem(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Set up the components need for all tests. + """ + Registry.create() + Registry().register(u'service_list', MagicMock()) + + with patch('openlp.core.lib.mediamanageritem.MediaManagerItem.__init__'), \ + patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__') : + self.media_item = SongMediaItem(MagicMock(), MagicMock()) + + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + # Not all tests use settings! + try: + os.unlink(self.ini_file) + os.unlink(Settings().fileName()) + except: + pass + + def build_song_footer_one_author_test(self): + """ + Test build songs footer with basic song and one author + """ + # GIVEN: A Song and a Service Item + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + + # WHEN: I generate the Footer with default settings + author_list = self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright'], + u'The array should be returned correctly with a song, one author and copyright') + self.assertEqual(author_list, [u'my author'], + u'The author list should be returned correctly with one author') + + def build_song_footer_two_authors_test(self): + """ + Test build songs footer with basic song and two authors + """ + # GIVEN: A Song and a Service Item + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_author = MagicMock() + mock_author.display_name = u'another author' + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + + # WHEN: I generate the Footer with default settings + author_list = self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author and another author', u'My copyright'], + u'The array should be returned correctly with a song, two authors and copyright') + self.assertEqual(author_list, [u'my author', u'another author'], + u'The author list should be returned correctly with two authors') + + def build_song_footer_base_ccli_test(self): + """ + Test build songs footer with basic song and two authors + """ + # GIVEN: A Song and a Service Item and a configured CCLI license + mock_song = MagicMock() + mock_song.title = u'My Song' + mock_author = MagicMock() + mock_author.display_name = u'my author' + mock_song.authors = [] + mock_song.authors.append(mock_author) + mock_song.copyright = u'My copyright' + service_item = ServiceItem(None) + Settings().setValue(u'core/ccli number', u'1234') + + # WHEN: I generate the Footer with default settings + self.media_item.generate_footer(service_item, mock_song) + + # THEN: I get the following Array returned + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 1234'], + u'The array should be returned correctly with a song, an author, copyright and ccli') + + # WHEN: I amend the CCLI value + Settings().setValue(u'core/ccli number', u'4321') + self.media_item.generate_footer(service_item, mock_song) + + # THEN: I would get an amended footer string + self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 4321'], + u'The array should be returned correctly with a song, an author, copyright and amended ccli') \ No newline at end of file From 0ef65fb8d2f9e2a758de47574b7b47f14d10a080 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 23 Apr 2013 20:17:25 +0100 Subject: [PATCH 189/235] minor fixes --- openlp/plugins/songs/forms/__init__.py | 2 +- tests/functional/openlp_plugins/songs/test_mediaitem.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/forms/__init__.py b/openlp/plugins/songs/forms/__init__.py index d6aef47dc..588f359f6 100644 --- a/openlp/plugins/songs/forms/__init__.py +++ b/openlp/plugins/songs/forms/__init__.py @@ -52,4 +52,4 @@ 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 editsongform import EditSongForm \ No newline at end of file +from editsongform import EditSongForm diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 5f5c161eb..a0f81bde8 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -26,7 +26,7 @@ class TestMediaItem(TestCase): Registry().register(u'service_list', MagicMock()) with patch('openlp.core.lib.mediamanageritem.MediaManagerItem.__init__'), \ - patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__') : + patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): self.media_item = SongMediaItem(MagicMock(), MagicMock()) fd, self.ini_file = mkstemp(u'.ini') @@ -42,7 +42,7 @@ class TestMediaItem(TestCase): try: os.unlink(self.ini_file) os.unlink(Settings().fileName()) - except: + except Exception: pass def build_song_footer_one_author_test(self): @@ -122,4 +122,4 @@ class TestMediaItem(TestCase): # THEN: I would get an amended footer string self.assertEqual(service_item.raw_footer, [u'My Song', u'my author', u'My copyright', u'CCLI License: 4321'], - u'The array should be returned correctly with a song, an author, copyright and amended ccli') \ No newline at end of file + u'The array should be returned correctly with a song, an author, copyright and amended ccli') From d3195dccf7294eda0808ddbcc05f5ce8a5c82e85 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 23 Apr 2013 21:31:19 +0100 Subject: [PATCH 190/235] Swap round urls --- openlp/plugins/remotes/html/openlp.js | 2 +- openlp/plugins/remotes/html/stage.js | 6 +++--- openlp/plugins/remotes/lib/httpserver.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 3cbe65366..10bc9e328 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -147,7 +147,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/stage/api/poll", + "/stage/poll", function (data, status) { var prevItem = OpenLP.currentItem; OpenLP.currentSlide = data.results.slide; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index dff51537c..42b7712f9 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( - "/stage/api/service/list", + "/stage/service/list", function (data, status) { OpenLP.nextSong = ""; $("#notes").html(""); @@ -46,7 +46,7 @@ window.OpenLP = { }, loadSlides: function (event) { $.getJSON( - "/stage/api/controller/live/text", + "/stage/controller/live/text", function (data, status) { OpenLP.currentSlides = data.results.slides; OpenLP.currentSlide = 0; @@ -137,7 +137,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/stage/api/poll", + "/stage/poll", function (data, status) { OpenLP.updateClock(data); if (OpenLP.currentItem != data.results.item || diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 878b197b3..eedc30102 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -267,11 +267,11 @@ class HttpRouter(object): (u'^/(stage)$', self.serve_file), (r'^/files/(.*)$', self.serve_file), (r'^/api/poll$', self.poll), - (r'^/stage/api/poll$', self.poll), + (r'^/stage/poll$', self.poll), (r'^/api/controller/(live|preview)/(.*)$', self.controller), - (r'^/stage/api/controller/(live|preview)/(.*)$', self.controller), + (r'^/stage/controller/(live|preview)/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), - (r'^/stage/api/service/(.*)$', self.service), + (r'^/stage/service/(.*)$', self.service), (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display), (r'^/api/alert$', self.alert), (r'^/api/plugin/(search)$', self.plugin_info), From 22541e729785ed93d15115404ae6b0c194d5edcf Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 24 Apr 2013 21:05:34 +0200 Subject: [PATCH 191/235] use true division (py3) --- openlp/core/lib/__init__.py | 9 +++++---- openlp/core/lib/dockwidget.py | 3 ++- openlp/core/lib/htmlbuilder.py | 4 ++-- openlp/core/lib/renderer.py | 12 ++++++------ openlp/core/lib/screen.py | 5 +++-- openlp/core/lib/searchedit.py | 6 +++--- openlp/core/ui/maindisplay.py | 7 ++++--- openlp/core/ui/themestab.py | 4 +++- 8 files changed, 28 insertions(+), 22 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 155c10141..14d67e914 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -30,6 +30,7 @@ The :mod:`lib` module contains most of the components and libraries that make OpenLP work. """ +from __future__ import division from distutils.version import LooseVersion import logging import os @@ -207,7 +208,7 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None): ext = os.path.splitext(thumb_path)[1].lower() reader = QtGui.QImageReader(image_path) if size is None: - ratio = float(reader.size().width()) / float(reader.size().height()) + ratio = reader.size().width() / reader.size().height() reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88)) else: reader.setScaledSize(size) @@ -260,8 +261,8 @@ def resize_image(image_path, width, height, background=u'#000000'): log.debug(u'resize_image - start') reader = QtGui.QImageReader(image_path) # The image's ratio. - image_ratio = float(reader.size().width()) / float(reader.size().height()) - resize_ratio = float(width) / float(height) + image_ratio = reader.size().width() / reader.size().height() + resize_ratio = width / height # Figure out the size we want to resize the image to (keep aspect ratio). if image_ratio == resize_ratio: size = QtCore.QSize(width, height) @@ -282,7 +283,7 @@ def resize_image(image_path, width, height, background=u'#000000'): new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied) painter = QtGui.QPainter(new_image) painter.fillRect(new_image.rect(), QtGui.QColor(background)) - painter.drawImage((width - real_width) / 2, (height - real_height) / 2, preview) + painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview) return new_image diff --git a/openlp/core/lib/dockwidget.py b/openlp/core/lib/dockwidget.py index 30182c901..15c116e0f 100644 --- a/openlp/core/lib/dockwidget.py +++ b/openlp/core/lib/dockwidget.py @@ -30,6 +30,7 @@ """ Provide additional functionality required by OpenLP from the inherited QDockWidget. """ +from __future__ import division import logging from PyQt4 import QtGui @@ -55,7 +56,7 @@ class OpenLPDockWidget(QtGui.QDockWidget): self.setWindowIcon(build_icon(icon)) # Sort out the minimum width. screens = ScreenList() - main_window_docbars = screens.current[u'size'].width() / 5 + main_window_docbars = screens.current[u'size'].width() // 5 if main_window_docbars > 300: self.setMinimumWidth(300) else: diff --git a/openlp/core/lib/htmlbuilder.py b/openlp/core/lib/htmlbuilder.py index 9c8b04076..d4e22b0dd 100644 --- a/openlp/core/lib/htmlbuilder.py +++ b/openlp/core/lib/htmlbuilder.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtWebKit @@ -276,7 +276,7 @@ def build_background_css(item, width): ``item`` Service Item containing theme and location information """ - width = int(width) / 2 + width = int(width) // 2 theme = item.themedata background = u'background-color: black' if theme: diff --git a/openlp/core/lib/renderer.py b/openlp/core/lib/renderer.py index c426e8871..5161246c0 100644 --- a/openlp/core/lib/renderer.py +++ b/openlp/core/lib/renderer.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtGui, QtCore, QtWebKit @@ -327,7 +327,7 @@ class Renderer(object): screen_size = self.screens.current[u'size'] self.width = screen_size.width() self.height = screen_size.height() - self.screen_ratio = float(self.height) / float(self.width) + self.screen_ratio = self.height / self.width log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio)) # 90% is start of footer self.footer_start = int(self.height * 0.90) @@ -546,15 +546,15 @@ class Renderer(object): """ smallest_index = 0 highest_index = len(html_list) - 1 - index = int(highest_index / 2) + index = highest_index // 2 while True: if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()): # We know that it does not fit, so change/calculate the new index and highest_index accordingly. highest_index = index - index = int(index - (index - smallest_index) / 2) + index = index - (index - smallest_index) // 2 else: smallest_index = index - index = int(index + (highest_index - index) / 2) + index = index + (highest_index - index) // 2 # We found the number of words which will fit. if smallest_index == index or highest_index == index: index = smallest_index @@ -582,7 +582,7 @@ class Renderer(object): html_list[0] = html_tags + html_list[0] smallest_index = 0 highest_index = len(html_list) - 1 - index = int(highest_index / 2) + index = highest_index // 2 return previous_html, previous_raw def _text_fits_on_slide(self, text): diff --git a/openlp/core/lib/screen.py b/openlp/core/lib/screen.py index 84e7e4258..146b492db 100644 --- a/openlp/core/lib/screen.py +++ b/openlp/core/lib/screen.py @@ -30,6 +30,7 @@ The :mod:`screen` module provides management functionality for a machines' displays. """ +from __future__ import division import logging import copy @@ -232,8 +233,8 @@ class ScreenList(object): ``window`` A QWidget we are finding the location of. """ - x = window.x() + (window.width() / 2) - y = window.y() + (window.height() / 2) + x = window.x() + (window.width() // 2) + y = window.y() + (window.height() // 2) for screen in self.screen_list: size = screen[u'size'] if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()): diff --git a/openlp/core/lib/searchedit.py b/openlp/core/lib/searchedit.py index 9623b0f53..a0c51cb74 100644 --- a/openlp/core/lib/searchedit.py +++ b/openlp/core/lib/searchedit.py @@ -26,7 +26,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### - +from __future__ import division import logging from PyQt4 import QtCore, QtGui @@ -85,10 +85,10 @@ class SearchEdit(QtGui.QLineEdit): size = self.clear_button.size() frame_width = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth) self.clear_button.move(self.rect().right() - frame_width - size.width(), - (self.rect().bottom() + 1 - size.height()) / 2) + (self.rect().bottom() + 1 - size.height()) // 2) if hasattr(self, u'menu_button'): size = self.menu_button.size() - self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) / 2) + self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2) def current_search_type(self): """ diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 2504520c0..02ae469d6 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -35,6 +35,7 @@ Some of the code for this form is based on the examples at: * `http://html5demos.com/two-videos`_ """ +from __future__ import division import cgi import logging import sys @@ -207,8 +208,8 @@ class MainDisplay(Display): painter_image.begin(self.initial_fame) painter_image.fillRect(self.initial_fame.rect(), background_color) painter_image.drawImage( - (self.screen[u'size'].width() - splash_image.width()) / 2, - (self.screen[u'size'].height() - splash_image.height()) / 2, + (self.screen[u'size'].width() - splash_image.width()) // 2, + (self.screen[u'size'].height() - splash_image.height()) // 2, splash_image) service_item = ServiceItem() service_item.bg_image_bytes = image_to_byte(self.initial_fame) @@ -268,7 +269,7 @@ class MainDisplay(Display): self.resize(self.width(), alert_height) self.setVisible(True) if location == AlertLocation.Middle: - self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) / 2) + self.move(self.screen[u'size'].left(), (self.screen[u'size'].height() - alert_height) // 2) elif location == AlertLocation.Bottom: self.move(self.screen[u'size'].left(), self.screen[u'size'].height() - alert_height) else: diff --git a/openlp/core/ui/themestab.py b/openlp/core/ui/themestab.py index f0f821494..d6dba2880 100644 --- a/openlp/core/ui/themestab.py +++ b/openlp/core/ui/themestab.py @@ -29,6 +29,8 @@ """ The Themes configuration tab """ +from __future__ import division + from PyQt4 import QtCore, QtGui from openlp.core.lib import Registry, Settings, SettingsTab, UiStrings, translate @@ -90,7 +92,7 @@ class ThemesTab(SettingsTab): self.global_level_label.setObjectName(u'global_level_label') self.level_layout.addRow(self.global_level_radio_button, self.global_level_label) label_top_margin = (self.song_level_radio_button.sizeHint().height() - - self.song_level_label.sizeHint().height()) / 2 + self.song_level_label.sizeHint().height()) // 2 for label in [self.song_level_label, self.service_level_label, self.global_level_label]: rect = label.rect() rect.setTop(rect.top() + label_top_margin) From 9d54944ad43643e6826e2712ead113a2fccef864 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 24 Apr 2013 22:47:30 +0200 Subject: [PATCH 192/235] use true division (py3) --- openlp/core/lib/settingstab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/settingstab.py b/openlp/core/lib/settingstab.py index d63f9c678..ab775599a 100644 --- a/openlp/core/lib/settingstab.py +++ b/openlp/core/lib/settingstab.py @@ -30,6 +30,7 @@ The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their own tab to the settings dialog. """ +from __future__ import division from PyQt4 import QtGui @@ -90,7 +91,7 @@ class SettingsTab(QtGui.QWidget): QtGui.QWidget.resizeEvent(self, event) width = self.width() - self.tab_layout.spacing() - \ self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right() - left_width = min(width - self.right_column.minimumSizeHint().width(), width / 2) + left_width = min(width - self.right_column.minimumSizeHint().width(), width // 2) left_width = max(left_width, self.left_column.minimumSizeHint().width()) self.left_column.setFixedWidth(left_width) From d7eb6b20c3452ebe0d041639d2d602632f770467 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Thu, 25 Apr 2013 19:58:37 +0200 Subject: [PATCH 193/235] Correct the naming of the preWizard -> pre_wizard. --- openlp/plugins/bibles/forms/bibleimportform.py | 2 +- openlp/plugins/bibles/forms/bibleupgradeform.py | 4 ++-- openlp/plugins/songs/forms/songexportform.py | 2 +- openlp/plugins/songs/forms/songimportform.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 6920be61c..0662f78db 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -511,7 +511,7 @@ class BibleImportForm(OpenLPWizard): name = bible[u'abbreviation'] self.web_bible_list[download_type][version] = name.strip() - def preWizard(self): + def pre_wizard(self): """ Prepare the UI for the import. """ diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index d8f329ee7..caba4b30f 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -105,7 +105,7 @@ class BibleUpgradeForm(OpenLPWizard): Perform necessary functions depending on which wizard page is active. """ if self.page(pageId) == self.progress_page: - self.preWizard() + self.pre_wizard() self.performWizard() self.post_wizard() elif self.page(pageId) == self.selectPage and not self.files: @@ -329,7 +329,7 @@ class BibleUpgradeForm(OpenLPWizard): self.cancel_button.setVisible(True) settings.endGroup() - def preWizard(self): + def pre_wizard(self): """ Prepare the UI for the upgrade. """ diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index f0554f588..36ed50cee 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -235,7 +235,7 @@ class SongExportForm(OpenLPWizard): self.availableListWidget.addItem(item) self.application.set_normal_cursor() - def preWizard(self): + def pre_wizard(self): """ Perform pre export tasks. """ diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py index 4c749f4d0..add306505 100644 --- a/openlp/plugins/songs/forms/songimportform.py +++ b/openlp/plugins/songs/forms/songimportform.py @@ -325,7 +325,7 @@ class SongImportForm(OpenLPWizard): self.error_copy_to_button.setHidden(True) self.error_save_to_button.setHidden(True) - def preWizard(self): + def pre_wizard(self): """ Perform pre import tasks """ From 84e9f6f1b1dbec12b6884f1ddbd78c93d3c25036 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 27 Apr 2013 11:54:57 +0200 Subject: [PATCH 194/235] Add test utils and move a function to there. --- tests/__init__.py | 0 .../openlp_core_lib/test_serviceitem.py | 49 ++++++------------- tests/utils/__init__.py | 0 tests/utils/constants.py | 5 ++ tests/utils/osdinteraction.py | 49 +++++++++++++++++++ 5 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/constants.py create mode 100644 tests/utils/osdinteraction.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index 26e9e7d44..cf3560d07 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -2,11 +2,12 @@ Package to test the openlp.core.lib package. """ import os -import cPickle from unittest import TestCase from mock import MagicMock, patch from openlp.core.lib import ItemCapabilities, ServiceItem, Registry +from tests.utils.osdinteraction import read_service_from_file +from tests.utils.constants import TEST_RESOURCES_PATH VERSE = u'The Lord said to {r}Noah{/r}: \n'\ @@ -18,8 +19,6 @@ VERSE = u'The Lord said to {r}Noah{/r}: \n'\ 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n' FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456'] -TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) - class TestServiceItem(TestCase): @@ -78,7 +77,7 @@ class TestServiceItem(TestCase): service_item.name = u'test' # WHEN: adding image to a service item - test_image = os.path.join(TEST_PATH, u'church.jpg') + test_image = os.path.join(TEST_RESOURCES_PATH, u'church.jpg') service_item.add_from_image(test_image, u'Image Title') # THEN: We should get back a valid service item @@ -133,8 +132,8 @@ class TestServiceItem(TestCase): service_item.name = u'test' # WHEN: adding image to a service item - test_file = os.path.join(TEST_PATH, u'church.jpg') - service_item.add_from_command(TEST_PATH, u'church.jpg', test_file) + test_file = os.path.join(TEST_RESOURCES_PATH, u'church.jpg') + service_item.add_from_command(TEST_RESOURCES_PATH, u'church.jpg', test_file) # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' @@ -151,7 +150,7 @@ class TestServiceItem(TestCase): assert len(service) == 2, u'The saved service should have two parts' assert service[u'header'][u'name'] == u'test', u'A test plugin should be returned' assert service[u'data'][0][u'title'] == u'church.jpg', u'The first title name should be "church,jpg"' - assert service[u'data'][0][u'path'] == TEST_PATH, u'The path should match the input path' + assert service[u'data'][0][u'path'] == TEST_RESOURCES_PATH, u'The path should match the input path' assert service[u'data'][0][u'image'] == test_file, u'The image should match the full path to image' # WHEN validating a service item @@ -170,13 +169,12 @@ class TestServiceItem(TestCase): """ Test the Service Item - adding a custom slide from a saved service """ - # GIVEN: A new service item and a mocked add icon function + # GIVEN: A new service item service_item = ServiceItem(None) - service_item.add_icon = MagicMock() # WHEN: adding a custom from a saved Service - line = self.convert_file_service_item(u'serviceitem_custom_1.osd') - service_item.set_from_service(line) + service = read_service_from_file(u'serviceitem_custom_1.osd') + service_item.set_from_service(service[0]) # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' @@ -195,18 +193,17 @@ class TestServiceItem(TestCase): """ Test the Service Item - adding an image from a saved service """ - # GIVEN: A new service item and a mocked add icon function + # GIVEN: A new service item image_name = u'image_1.jpg' - test_file = os.path.join(TEST_PATH, image_name) + test_file = os.path.join(TEST_RESOURCES_PATH, image_name) frame_array = {u'path': test_file, u'title': image_name} service_item = ServiceItem(None) - service_item.add_icon = MagicMock() # WHEN: adding an image from a saved Service and mocked exists - line = self.convert_file_service_item(u'serviceitem_image_1.osd') + service = read_service_from_file(u'serviceitem_image_1.osd') with patch('os.path.exists'): - service_item.set_from_service(line, TEST_PATH) + service_item.set_from_service(service[0], TEST_RESOURCES_PATH) # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' @@ -230,7 +227,7 @@ class TestServiceItem(TestCase): """ Test the Service Item - adding an image from a saved local service """ - # GIVEN: A new service item and a mocked add icon function + # GIVEN: A new service item image_name1 = u'image_1.jpg' image_name2 = u'image_2.jpg' test_file1 = os.path.join(u'/home/openlp', image_name1) @@ -239,12 +236,11 @@ class TestServiceItem(TestCase): frame_array2 = {u'path': test_file2, u'title': image_name2} service_item = ServiceItem(None) - service_item.add_icon = MagicMock() # WHEN: adding an image from a saved Service and mocked exists - line = self.convert_file_service_item(u'serviceitem_image_2.osd') + service = read_service_from_file(u'serviceitem_image_2.osd') with patch('os.path.exists'): - service_item.set_from_service(line) + service_item.set_from_service(service[0]) # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' @@ -267,16 +263,3 @@ class TestServiceItem(TestCase): u'This service item should be able to be run in a can be made to Loop' assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \ u'This service item should be able to have new items added to it' - - def convert_file_service_item(self, name): - service_file = os.path.join(TEST_PATH, name) - try: - open_file = open(service_file, u'r') - items = cPickle.load(open_file) - first_line = items[0] - except IOError: - first_line = u'' - finally: - open_file.close() - return first_line - diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/utils/constants.py b/tests/utils/constants.py new file mode 100644 index 000000000..56a7a78ca --- /dev/null +++ b/tests/utils/constants.py @@ -0,0 +1,5 @@ + +import os + +OPENLP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..')) +TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources') \ No newline at end of file diff --git a/tests/utils/osdinteraction.py b/tests/utils/osdinteraction.py new file mode 100644 index 000000000..f275d18c2 --- /dev/null +++ b/tests/utils/osdinteraction.py @@ -0,0 +1,49 @@ +# -*- 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:`osdinteraction` provides miscellaneous functions for interacting with +OSD files. +""" + +import os +import cPickle + +from tests.utils.constants import TEST_RESOURCES_PATH + + +def read_service_from_file(file_name): + """ + Reads an OSD file and returns the first service item found therein. + @param file_name: File name of an OSD file residing in the tests/resources folder. + @return: The service contained in the file. + """ + service_file = os.path.join(TEST_RESOURCES_PATH, file_name) + with open(service_file, u'r') as open_file: + service = cPickle.load(open_file) + return service From 154c82f5ea623d7c83731efcf1694f04712d62f4 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 27 Apr 2013 12:00:27 +0200 Subject: [PATCH 195/235] Rename a method and remove a non needed registry var. --- openlp/core/ui/listpreviewwidget.py | 12 +----------- openlp/core/ui/slidecontroller.py | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index d259d43e7..94385dd91 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -86,7 +86,7 @@ class ListPreviewWidget(QtGui.QTableWidget): self.screen_ratio = screen_ratio self.__recalculate_layout() - def replace_service_manager_item(self, service_item, width, slideNumber): + def replace_service_item(self, service_item, width, slideNumber): """ Replaces the current preview items with the ones in service_item. Displays the given slide. @@ -170,13 +170,3 @@ class ListPreviewWidget(QtGui.QTableWidget): image_manager = property(_get_image_manager) - def _get_main_window(self): - """ - Adds the main window to the class dynamically. - """ - if not hasattr(self, u'_main_window'): - self._main_window = Registry().get(u'main_window') - return self._main_window - - main_window = property(_get_main_window) - diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 2c24faca0..e1b8b8183 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -784,7 +784,7 @@ class SlideController(DisplayController): if not self.service_item.is_command() and framenumber == slideno: self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'], ImageSource.ImagePlugin) - self.preview_widget.replace_service_manager_item(self.service_item, width, slideno) + self.preview_widget.replace_service_item(self.service_item, width, slideno) self.enableToolBar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme From 46882beefeb3f76884fba7841e6eb085aa70e9ad Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 27 Apr 2013 12:00:46 +0200 Subject: [PATCH 196/235] Add tests for the listpreviewwidget. --- .../openlp_core_ui/test_listpreviewwidget.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/interfaces/openlp_core_ui/test_listpreviewwidget.py diff --git a/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py b/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py new file mode 100644 index 000000000..afad91ab7 --- /dev/null +++ b/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py @@ -0,0 +1,88 @@ +""" + Package to test the openlp.core.ui.listpreviewwidget. +""" + +from unittest import TestCase +from mock import MagicMock, patch + +from PyQt4 import QtGui + +from openlp.core.lib import Registry, ServiceItem +from openlp.core.ui import listpreviewwidget +from tests.utils.osdinteraction import read_service_from_file + +class TestServiceManager(TestCase): + + def setUp(self): + """ + Create the UI. + """ + Registry.create() + self.app = QtGui.QApplication([]) + self.main_window = QtGui.QMainWindow() + self.image = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32) + self.image_manager = MagicMock() + self.image_manager.get_image.return_value = self.image + Registry().register(u'image_manager', self.image_manager) + self.preview_widget = listpreviewwidget.ListPreviewWidget(self.main_window, 2) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault. + """ + del self.preview_widget + del self.main_window + del self.app + + def initial_slide_count_test(self): + """ + Test the inital slide count. + """ + # GIVEN: A new ListPreviewWidget instance. + # WHEN: No SlideItem has been added yet. + # THEN: The count of items should be zero. + self.assertEqual(self.preview_widget.slide_count(), 0, + u'The slide list should be empty.') + + def initial_slide_number_test(self): + """ + Test the inital slide number. + """ + # GIVEN: A new ListPreviewWidget instance. + # WHEN: No SlideItem has been added yet. + # THEN: The number of the current item should be -1. + self.assertEqual(self.preview_widget.current_slide_number(), -1, + u'The slide number should be -1.') + + def replace_service_item_test(self): + """ + Test item counts and current number with a service item. + """ + # GIVEN: A ServiceItem with two frames. + service_item = ServiceItem(None) + service = read_service_from_file(u'serviceitem_image_2.osd') + with patch('os.path.exists'): + service_item.set_from_service(service[0]) + # WHEN: Added to the preview widget. + self.preview_widget.replace_service_item(service_item, 1, 1) + # THEN: The slide count and number should fit. + self.assertEqual(self.preview_widget.slide_count(), 2, + u'The slide count should be 2.') + self.assertEqual(self.preview_widget.current_slide_number(), 1, + u'The current slide number should be 1.') + + def change_slide_test(self): + """ + Test the change_slide method. + """ + # GIVEN: A ServiceItem with two frames content. + service_item = ServiceItem(None) + service = read_service_from_file(u'serviceitem_image_2.osd') + with patch('os.path.exists'): + service_item.set_from_service(service[0]) + # WHEN: Added to the preview widget and switched to the second frame. + self.preview_widget.replace_service_item(service_item, 1, 0) + self.preview_widget.change_slide(1) + # THEN: The current_slide_number should reflect the change. + self.assertEqual(self.preview_widget.current_slide_number(), 1, + u'The current slide number should be 1.') From c0d722e63ac8bf2b545e7def0ab6147f7574a65e Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 27 Apr 2013 12:06:08 +0200 Subject: [PATCH 197/235] Name listpreviewwidget test class correctly. --- tests/interfaces/openlp_core_ui/test_listpreviewwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py b/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py index afad91ab7..8a25c0b4f 100644 --- a/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/interfaces/openlp_core_ui/test_listpreviewwidget.py @@ -11,7 +11,7 @@ from openlp.core.lib import Registry, ServiceItem from openlp.core.ui import listpreviewwidget from tests.utils.osdinteraction import read_service_from_file -class TestServiceManager(TestCase): +class TestListPreviewWidget(TestCase): def setUp(self): """ From 4c2f078bf185574537564b60fac407ab90523d3c Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 27 Apr 2013 22:59:56 +0200 Subject: [PATCH 198/235] Add a missing \n at EOF. Add a comment. --- openlp/core/ui/listpreviewwidget.py | 7 ++++--- tests/utils/constants.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 94385dd91..ca04c9688 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -44,9 +44,7 @@ class ListPreviewWidget(QtGui.QTableWidget): One needs to call replace_service_manager_item() to make this widget display something. """ super(QtGui.QTableWidget, self).__init__(parent) - self.service_item = ServiceItem() - self.screen_ratio = screen_ratio - + # Set up the widget. self.setColumnCount(1) self.horizontalHeader().setVisible(False) self.setColumnWidth(0, parent.width()) @@ -55,6 +53,9 @@ class ListPreviewWidget(QtGui.QTableWidget): self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setAlternatingRowColors(True) + # Initialize variables. + self.service_item = ServiceItem() + self.screen_ratio = screen_ratio def resizeEvent(self, QResizeEvent): """ diff --git a/tests/utils/constants.py b/tests/utils/constants.py index 56a7a78ca..4b28fcc83 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -2,4 +2,4 @@ import os OPENLP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..')) -TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources') \ No newline at end of file +TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources') From 2d22e8420a4f08f21de84d9884f066c274a182a1 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sun, 28 Apr 2013 19:17:45 +0200 Subject: [PATCH 199/235] do not pass QImages to build_icon (if not needed) --- openlp/core/lib/__init__.py | 2 +- openlp/core/ui/servicemanager.py | 4 ++-- openlp/plugins/alerts/forms/alertdialog.py | 6 +++--- openlp/plugins/media/lib/mediaitem.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 155c10141..9a367c9b5 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -155,7 +155,7 @@ def build_icon(icon): ``icon`` The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file - location like ``/path/to/file.png``. + location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. """ button_icon = QtGui.QIcon() if isinstance(icon, QtGui.QIcon): diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 43280aef8..1929f444c 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -295,8 +295,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): Sets up the service manager, toolbars, list view, et al. """ QtGui.QWidget.__init__(self, parent) - self.active = build_icon(QtGui.QImage(u':/media/auto-start_active.png')) - self.inactive = build_icon(QtGui.QImage(u':/media/auto-start_inactive.png')) + self.active = build_icon(u':/media/auto-start_active.png') + self.inactive = build_icon(u':/media/auto-start_inactive.png') Registry().register(u'service_manager', self) self.service_items = [] self.suffixes = [] diff --git a/openlp/plugins/alerts/forms/alertdialog.py b/openlp/plugins/alerts/forms/alertdialog.py index e8a50d183..db2579298 100644 --- a/openlp/plugins/alerts/forms/alertdialog.py +++ b/openlp/plugins/alerts/forms/alertdialog.py @@ -75,9 +75,9 @@ class Ui_AlertDialog(object): self.manage_button_layout.addWidget(self.delete_button) self.manage_button_layout.addStretch() self.alert_dialog_layout.addLayout(self.manage_button_layout, 1, 1) - displayIcon = build_icon(u':/general/general_live.png') - self.display_button = create_button(alert_dialog, u'display_button', icon=displayIcon, enabled=False) - self.display_close_button = create_button(alert_dialog, u'display_close_button', icon=displayIcon, + display_icon = build_icon(u':/general/general_live.png') + self.display_button = create_button(alert_dialog, u'display_button', icon=display_icon, enabled=False) + self.display_close_button = create_button(alert_dialog, u'display_close_button', icon=display_icon, enabled=False) self.button_box = create_button_box(alert_dialog, u'button_box', [u'close'], [self.display_button, self.display_close_button]) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 2037346ad..243fcd158 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_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')) +VIDEO_ICON = build_icon(u':/media/media_video.png') +AUDIO_ICON = build_icon(u':/media/media_audio.png') +DVD_ICON = build_icon(u':/media/media_video.png') +ERROR_ICON = build_icon(u':/general/general_delete.png') class MediaMediaItem(MediaManagerItem): From 75c8382188a0ff39d1ac516d4d14b0df256ec3c6 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sun, 12 May 2013 14:02:48 +0200 Subject: [PATCH 200/235] added test --- tests/functional/openlp_core_lib/test_settings.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 786a884a0..754d82256 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -30,6 +30,19 @@ class TestSettings(TestCase): os.unlink(self.ini_file) os.unlink(Settings().fileName()) + def extend_default_settings_test(self): + """ + Test the static extend_default_settings() method. + """ + with self.assertRaises(KeyError) as context: + Settings().value(u'core/does not exist') + self.assertEqual(context.exception[0], u'core/does not exist') + + Settings.extend_default_settings({u'core/does exist': True}) + value = Settings().value(u'core/does exist') + assert value and isinstance(value, bool) + + def settings_basic_test(self): """ Test the Settings creation and its default usage From d03a9a8eeca21eae468a177b6c5339bf9f780c56 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sun, 12 May 2013 14:04:15 +0200 Subject: [PATCH 201/235] added comments --- tests/functional/openlp_core_lib/test_settings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 754d82256..714ee6fb5 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -32,14 +32,24 @@ class TestSettings(TestCase): def extend_default_settings_test(self): """ - Test the static extend_default_settings() method. + Test the static extend_default_settings() method """ + # GIVEN: + + # WHEN: Try to access not existing setting. with self.assertRaises(KeyError) as context: Settings().value(u'core/does not exist') + + # THEN: An exception should be raised. self.assertEqual(context.exception[0], u'core/does not exist') + # GIVEN: Extended setting. Settings.extend_default_settings({u'core/does exist': True}) + + # WHEN: Try to access it. value = Settings().value(u'core/does exist') + + # THEN: The correct value should be returned. assert value and isinstance(value, bool) From d323f9d746adb00b179092b65d2f3bd886cd2477 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 14 May 2013 15:58:07 +0200 Subject: [PATCH 202/235] Reverted 'adding multiple service items' functionality --- openlp/plugins/images/lib/mediaitem.py | 119 +++++++++++-------------- 1 file changed, 53 insertions(+), 66 deletions(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index e8a81d886..dc2aa3660 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -32,7 +32,7 @@ import os from PyQt4 import QtCore, QtGui -from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, ServiceItem, ServiceItemContext, Settings, \ +from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, ServiceItemContext, Settings, \ 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 @@ -538,76 +538,63 @@ class ImageMediaItem(MediaManagerItem): """ background = QtGui.QColor(Settings().value(self.settings_section + u'/background color')) if item: - new_items = [item] + items = [item] else: - new_items = self.list_view.selectedItems() - if not new_items: + items = self.list_view.selectedItems() + if not items: return False - for bitem in new_items: - new_service_item = ServiceItem(self.plugin) - new_service_item.add_icon(self.plugin.icon_path) - # Determine service item title - if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups): - new_service_item.title = bitem.text(0) - else: - new_service_item.title = unicode(self.plugin.name_strings[u'plural']) - new_service_item.add_capability(ItemCapabilities.CanMaintain) - new_service_item.add_capability(ItemCapabilities.CanPreview) - new_service_item.add_capability(ItemCapabilities.CanLoop) - new_service_item.add_capability(ItemCapabilities.CanAppend) - # force a nonexistent theme - new_service_item.theme = -1 - sub_images = [] - missing_items = [] - missing_items_filenames = [] - # Expand groups to images + # Determine service item title + if isinstance(items[0].data(0, QtCore.Qt.UserRole), ImageGroups): + service_item.title = items[0].text(0) + else: + service_item.title = unicode(self.plugin.name_strings[u'plural']) + service_item.add_capability(ItemCapabilities.CanMaintain) + service_item.add_capability(ItemCapabilities.CanPreview) + service_item.add_capability(ItemCapabilities.CanLoop) + service_item.add_capability(ItemCapabilities.CanAppend) + # force a nonexistent theme + service_item.theme = -1 + missing_items = [] + missing_items_filenames = [] + # Expand groups to images + for bitem in items: if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None: for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): - sub_images.append(bitem.child(index)) - if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): - sub_images.append(bitem) - # Don't try to display empty groups - if not sub_images: - return False - # Find missing files - for bitem in sub_images: - filename = bitem.data(0, QtCore.Qt.UserRole).filename - if not os.path.exists(filename): - missing_items.append(bitem) - missing_items_filenames.append(filename) - for item in missing_items: - sub_images.remove(item) - # We cannot continue, as all images do not exist. - if not sub_images: - if not remote: - critical_error_message_box( - translate('ImagePlugin.MediaItem', 'Missing Image(s)'), - translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s') % - u'\n'.join(missing_items_filenames)) - return False - # We have missing as well as existing images. We ask what to do. - elif missing_items and QtGui.QMessageBox.question(self, - translate('ImagePlugin.MediaItem', 'Missing Image(s)'), - translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' - 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == \ - QtGui.QMessageBox.No: - return False - # Continue with the existing images. - for sub_image in sub_images: - filename = sub_image.data(0, QtCore.Qt.UserRole).filename - name = os.path.split(filename)[1] - new_service_item.add_from_image(filename, name, background) - # Add the service item to the correct controller - if context == ServiceItemContext.Service: - self.service_manager.add_service_item(new_service_item) - elif context == ServiceItemContext.Preview: - self.preview_controller.add_service_item(new_service_item) - elif context == ServiceItemContext.Live: - self.live_controller.add_service_item(new_service_item) - # Return False because we added the service item ourselves - return False + items.append(bitem.child(index)) + items.remove(bitem) + # Don't try to display empty groups + if not items: + return False + # Find missing files + for bitem in items: + filename = bitem.data(0, QtCore.Qt.UserRole).filename + if not os.path.exists(filename): + missing_items.append(bitem) + missing_items_filenames.append(filename) + for item in missing_items: + items.remove(item) + # We cannot continue, as all images do not exist. + if not items: + if not remote: + critical_error_message_box( + translate('ImagePlugin.MediaItem', 'Missing Image(s)'), + translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s') % + u'\n'.join(missing_items_filenames)) + return False + # We have missing as well as existing images. We ask what to do. + elif missing_items and QtGui.QMessageBox.question(self, + translate('ImagePlugin.MediaItem', 'Missing Image(s)'), + translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' + 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: + return False + # Continue with the existing images. + for bitem in items: + filename = bitem.data(0, QtCore.Qt.UserRole).filename + name = os.path.split(filename)[1] + service_item.add_from_image(filename, name, background) + return True def check_group_exists(self, new_group): """ From 71335357dd8a3178b1873af4a7a5064171dc9fbc Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 14 May 2013 16:17:56 +0200 Subject: [PATCH 203/235] - Fixed traceback on adding multiple image groups to the service in one go --- openlp/plugins/images/lib/mediaitem.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index dc2aa3660..cb4656539 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -554,28 +554,23 @@ class ImageMediaItem(MediaManagerItem): service_item.add_capability(ItemCapabilities.CanAppend) # force a nonexistent theme service_item.theme = -1 - missing_items = [] missing_items_filenames = [] + images_filenames = [] # Expand groups to images for bitem in items: if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None: for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): - items.append(bitem.child(index)) - items.remove(bitem) + images_filenames.append(bitem.child(index).data(0, QtCore.Qt.UserRole).filename) # Don't try to display empty groups - if not items: + if not images_filenames: return False # Find missing files - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename + for filename in images_filenames: if not os.path.exists(filename): - missing_items.append(bitem) missing_items_filenames.append(filename) - for item in missing_items: - items.remove(item) # We cannot continue, as all images do not exist. - if not items: + if not images_filenames: if not remote: critical_error_message_box( translate('ImagePlugin.MediaItem', 'Missing Image(s)'), @@ -583,15 +578,14 @@ class ImageMediaItem(MediaManagerItem): u'\n'.join(missing_items_filenames)) return False # We have missing as well as existing images. We ask what to do. - elif missing_items and QtGui.QMessageBox.question(self, + elif missing_items_filenames and QtGui.QMessageBox.question(self, translate('ImagePlugin.MediaItem', 'Missing Image(s)'), translate('ImagePlugin.MediaItem', 'The following image(s) no longer exist: %s\n' 'Do you want to add the other images anyway?') % u'\n'.join(missing_items_filenames), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No | QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.No: return False # Continue with the existing images. - for bitem in items: - filename = bitem.data(0, QtCore.Qt.UserRole).filename + for filename in images_filenames: name = os.path.split(filename)[1] service_item.add_from_image(filename, name, background) return True From 673a87a65d14042632d5c7f6ea8802a4f8a0ff24 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 14 May 2013 16:48:49 +0200 Subject: [PATCH 204/235] - Fixed a bug where new image groups added from the 'choose group' popup weren't added to the 'add group' and 'choose group' popup selectboxes --- openlp/plugins/images/lib/mediaitem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index cb4656539..8a564fa2c 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -440,6 +440,8 @@ class ImageMediaItem(MediaManagerItem): parent_group.parent_id = 0 parent_group.group_name = self.choose_group_form.new_group_edit.text() self.manager.save_object(parent_group) + self.fill_groups_combobox(self.choose_group_form.group_combobox) + self.fill_groups_combobox(self.add_group_form.parent_group_combobox) else: parent_group = target_group.data(0, QtCore.Qt.UserRole) if isinstance(parent_group, ImageFilenames): From 6572ab31e818e254843c68b11baf866c97e833c2 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Tue, 14 May 2013 17:05:41 +0200 Subject: [PATCH 205/235] - Fixed adding single images again --- openlp/plugins/images/lib/mediaitem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 328ba462a..deed14594 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -561,6 +561,8 @@ class ImageMediaItem(MediaManagerItem): for index in range(0, bitem.childCount()): if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames): images_filenames.append(bitem.child(index).data(0, QtCore.Qt.UserRole).filename) + elif isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): + images_filenames.append(bitem.data(0, QtCore.Qt.UserRole).filename) # Don't try to display empty groups if not images_filenames: return False From 7a42cd3769ac06eb55cf956ccb34cb795f84b703 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 18 May 2013 10:44:03 +0200 Subject: [PATCH 206/235] added test --- openlp/core/lib/__init__.py | 3 +- tests/functional/openlp_core_lib/test_lib.py | 61 ++++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 911ba4703..d6c338271 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -203,7 +203,8 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None): States if an icon should be build and returned from the thumb. Defaults to ``True``. ``size`` - Allows to state a own size to use. Defaults to ``None``, which means that a default height of 88 is used. + Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default height of 88 + is used. """ ext = os.path.splitext(thumb_path)[1].lower() reader = QtGui.QImageReader(image_path) diff --git a/tests/functional/openlp_core_lib/test_lib.py b/tests/functional/openlp_core_lib/test_lib.py index c03a11265..b0d1a4bed 100644 --- a/tests/functional/openlp_core_lib/test_lib.py +++ b/tests/functional/openlp_core_lib/test_lib.py @@ -1,13 +1,20 @@ """ Package to test the openlp.core.lib package. """ +import os + from unittest import TestCase from datetime import datetime, timedelta from mock import MagicMock, patch +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import str_to_bool, create_thumb, translate, check_directory_exists, get_text_file_string, \ + build_icon, image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags + + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources')) -from openlp.core.lib import str_to_bool, translate, check_directory_exists, get_text_file_string, build_icon, \ - image_to_byte, check_item_selected, validate_thumb, create_separated_list, clean_tags, expand_tags class TestLib(TestCase): @@ -125,7 +132,7 @@ class TestLib(TestCase): Test the check_directory_exists() function """ with patch(u'openlp.core.lib.os.path.exists') as mocked_exists, \ - patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs: + patch(u'openlp.core.lib.os.makedirs') as mocked_makedirs: # GIVEN: A directory to check and a mocked out os.makedirs and os.path.exists directory_to_check = u'existing/directory' @@ -219,7 +226,7 @@ class TestLib(TestCase): Test the build_icon() function with a resource URI """ with patch(u'openlp.core.lib.QtGui') as MockedQtGui, \ - patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap: + patch(u'openlp.core.lib.QtGui.QPixmap') as MockedQPixmap: # GIVEN: A mocked QIcon and a mocked QPixmap MockedQtGui.QIcon = MagicMock MockedQtGui.QIcon.Normal = 1 @@ -261,9 +268,43 @@ class TestLib(TestCase): mocked_byte_array.toBase64.assert_called_with() assert result == u'base64mock', u'The result should be the return value of the mocked out base64 method' + def create_thumb_with_size_test(self): + """ + Test the create_thumb() function + """ + # GIVEN: An image to create a thumb of. + image_path = os.path.join(TEST_PATH, u'church.jpg') + thumb_path = os.path.join(TEST_PATH, u'church_thumb.jpg') + thumb_size = QtCore.QSize(10, 20) + + # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the + # last test. + try: + os.remove(thumb_path) + except: + pass + + # Only continue when the thumb does not exist. + assert not os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.' + + # WHEN: Create the thumb. + icon = create_thumb(image_path, thumb_path, size=thumb_size) + + # THEN: Check if the thumb was created. + assert os.path.exists(thumb_path), u'Test was not ran, because the thumb already exists.' + assert isinstance(icon, QtGui.QIcon), u'The icon should be a QIcon.' + assert not icon.isNull(), u'The icon should not be null.' + assert QtGui.QImageReader(thumb_path).size() == thumb_size, u'The thumb should have the given size.' + + # Remove the thumb so that the test actually tests if the thumb will be created. + try: + os.remove(thumb_path) + except: + pass + def check_item_selected_true_test(self): """ - Test that the check_item_selected() function returns True when there are selected indexes. + Test that the check_item_selected() function returns True when there are selected indexes """ # GIVEN: A mocked out QtGui module and a list widget with selected indexes MockedQtGui = patch(u'openlp.core.lib.QtGui') @@ -423,7 +464,7 @@ class TestLib(TestCase): def create_separated_list_qlocate_test(self): """ - Test the create_separated_list function using the Qt provided method. + Test the create_separated_list function using the Qt provided method """ with patch(u'openlp.core.lib.Qt') as mocked_qt, \ patch(u'openlp.core.lib.QtCore.QLocale.createSeparatedList') as mocked_createSeparatedList: @@ -442,7 +483,7 @@ class TestLib(TestCase): def create_separated_list_empty_list_test(self): """ - Test the create_separated_list function with an empty list. + Test the create_separated_list function with an empty list """ with patch(u'openlp.core.lib.Qt') as mocked_qt: # GIVEN: An empty list and the mocked Qt module. @@ -458,7 +499,7 @@ class TestLib(TestCase): def create_separated_list_with_one_item_test(self): """ - Test the create_separated_list function with a list consisting of only one entry. + Test the create_separated_list function with a list consisting of only one entry """ with patch(u'openlp.core.lib.Qt') as mocked_qt: # GIVEN: A list with a string and the mocked Qt module. @@ -474,7 +515,7 @@ class TestLib(TestCase): def create_separated_list_with_two_items_test(self): """ - Test the create_separated_list function with a list of two entries. + Test the create_separated_list function with a list of two entries """ with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate: # GIVEN: A list of strings and the mocked Qt module. @@ -491,7 +532,7 @@ class TestLib(TestCase): def create_separated_list_with_three_items_test(self): """ - Test the create_separated_list function with a list of three items. + Test the create_separated_list function with a list of three items """ with patch(u'openlp.core.lib.Qt') as mocked_qt, patch(u'openlp.core.lib.translate') as mocked_translate: # GIVEN: A list with a string and the mocked Qt module. From 6e66edce03060b6c2f5823c7fdea3577b5c78229 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sat, 18 May 2013 10:49:21 +0200 Subject: [PATCH 207/235] removed test --- .../openlp_core_lib/test_settings.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 714ee6fb5..786a884a0 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -30,29 +30,6 @@ class TestSettings(TestCase): os.unlink(self.ini_file) os.unlink(Settings().fileName()) - def extend_default_settings_test(self): - """ - Test the static extend_default_settings() method - """ - # GIVEN: - - # WHEN: Try to access not existing setting. - with self.assertRaises(KeyError) as context: - Settings().value(u'core/does not exist') - - # THEN: An exception should be raised. - self.assertEqual(context.exception[0], u'core/does not exist') - - # GIVEN: Extended setting. - Settings.extend_default_settings({u'core/does exist': True}) - - # WHEN: Try to access it. - value = Settings().value(u'core/does exist') - - # THEN: The correct value should be returned. - assert value and isinstance(value, bool) - - def settings_basic_test(self): """ Test the Settings creation and its default usage From 255ed90d972d2565abe929be8f9913c2a1d34f0c Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Sun, 19 May 2013 13:31:02 +0200 Subject: [PATCH 208/235] Change helper method to private --- tests/functional/openlp_plugins/images/test_lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_plugins/images/test_lib.py b/tests/functional/openlp_plugins/images/test_lib.py index 2e9432eaf..0aecc0a7f 100644 --- a/tests/functional/openlp_plugins/images/test_lib.py +++ b/tests/functional/openlp_plugins/images/test_lib.py @@ -126,7 +126,7 @@ class TestImageMediaItem(TestCase): self.media_item.reset_action.setVisible.assert_called_with(False) self.media_item.live_controller.display.reset_image.assert_called_with() - def recursively_delete_group_side_effect(*args, **kwargs): + def _recursively_delete_group_side_effect(*args, **kwargs): """ Side effect method that creates custom retun values for the recursively_delete_group method """ @@ -160,7 +160,7 @@ class TestImageMediaItem(TestCase): ImageFilenames.group_id = 1 ImageGroups.parent_id = 1 self.media_item.manager = MagicMock() - self.media_item.manager.get_all_objects.side_effect = self.recursively_delete_group_side_effect + self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect self.media_item.servicePath = "" test_group = ImageGroups() test_group.id = 1 From 111304495502b57752de708b6c9767678c81d287 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 19 May 2013 20:56:48 +0100 Subject: [PATCH 209/235] Add basic Live display to remote --- openlp/core/ui/slidecontroller.py | 25 ++++--- openlp/plugins/remotes/html/live.html | 42 +++++++++++ openlp/plugins/remotes/html/live.js | 92 ++++++++++++++++++++++++ openlp/plugins/remotes/lib/httpserver.py | 16 +++++ 4 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 openlp/plugins/remotes/html/live.html create mode 100644 openlp/plugins/remotes/html/live.js diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 86b114e1e..e2d318549 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -136,6 +136,8 @@ class SlideController(DisplayController): self.keypress_loop = False self.category = UiStrings().LiveToolbar ActionList.get_instance().add_category(unicode(self.category), CategoryOrder.standard_toolbar) + self.slide_count = 0 + self.slide_image = None else: Registry().register(u'preview_controller', self) self.type_label.setText(UiStrings().Preview) @@ -1050,27 +1052,28 @@ class SlideController(DisplayController): def updatePreview(self): """ - This updates the preview frame, for example after changing a slide or - using *Blank to Theme*. + This updates the preview frame, for example after changing a slide or using *Blank to Theme*. """ log.debug(u'updatePreview %s ' % self.screens.current[u'primary']) if not self.screens.current[u'primary'] and self.service_item and \ self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): - # Grab now, but try again in a couple of seconds if slide change - # is slow - QtCore.QTimer.singleShot(0.5, self.grabMainDisplay) - QtCore.QTimer.singleShot(2.5, self.grabMainDisplay) + # Grab now, but try again in a couple of seconds if slide change is slow + QtCore.QTimer.singleShot(0.5, self.grab_maindisplay) + QtCore.QTimer.singleShot(2.5, self.grab_maindisplay) else: - self.slidePreview.setPixmap(self.display.preview()) + self.slide_image = self.display.preview() + self.slidePreview.setPixmap(self.slide_image) + self.slide_count += 1 - def grabMainDisplay(self): + def grab_maindisplay(self): """ Creates an image of the current screen and updates the preview frame. """ - winid = QtGui.QApplication.desktop().winId() + win_id = QtGui.QApplication.desktop().winId() rect = self.screens.current[u'size'] - winimg = QtGui.QPixmap.grabWindow(winid, rect.x(), rect.y(), rect.width(), rect.height()) - self.slidePreview.setPixmap(winimg) + win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height()) + self.slidePreview.setPixmap(win_image) + self.slide_image = win_image def on_slide_selected_next_action(self, checked): """ diff --git a/openlp/plugins/remotes/html/live.html b/openlp/plugins/remotes/html/live.html new file mode 100644 index 000000000..2f505f98e --- /dev/null +++ b/openlp/plugins/remotes/html/live.html @@ -0,0 +1,42 @@ + + + + + + ${live_title} + + + + + + + +
A
+ + \ No newline at end of file diff --git a/openlp/plugins/remotes/html/live.js b/openlp/plugins/remotes/html/live.js new file mode 100644 index 000000000..a44ce02ca --- /dev/null +++ b/openlp/plugins/remotes/html/live.js @@ -0,0 +1,92 @@ +/****************************************************************************** + * 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 * + ******************************************************************************/ +window.OpenLP = { + loadSlide: function (event) { + $.getJSON( + "/live/image", + function (data, status) { + OpenLP.currentSlides = data.results.slides; + OpenLP.currentSlide = 0; + OpenLP.currentTags = Array(); + var div = $("#verseorder"); + div.html(""); + var tag = ""; + var tags = 0; + var lastChange = 0; + $.each(data.results.slides, function(idx, slide) { + var prevtag = tag; + tag = slide["tag"]; + if (tag != prevtag) { + // If the tag has changed, add new one to the list + lastChange = idx; + tags = tags + 1; + div.append(" "); + $("#verseorder span").last().attr("id", "tag" + tags).text(tag); + } + else { + if ((slide["text"] == data.results.slides[lastChange]["text"]) && + (data.results.slides.length > idx + (idx - lastChange))) { + // If the tag hasn't changed, check to see if the same verse + // has been repeated consecutively. Note the verse may have been + // split over several slides, so search through. If so, repeat the tag. + var match = true; + for (var idx2 = 0; idx2 < idx - lastChange; idx2++) { + if(data.results.slides[lastChange + idx2]["text"] != data.results.slides[idx + idx2]["text"]) { + match = false; + break; + } + } + if (match) { + lastChange = idx; + tags = tags + 1; + div.append(" "); + $("#verseorder span").last().attr("id", "tag" + tags).text(tag); + } + } + } + OpenLP.currentTags[idx] = tags; + if (slide["selected"]) + OpenLP.currentSlide = idx; + }) + } + ); + }, + pollServer: function () { + $.getJSON( + "/live/poll", + function (data, status) { + if (OpenLP.slideCount != data.results.slide_count) { + OpenLP.slideCount = data.results.slide_count; + OpenLP.loadSlide(); + } + } + ); + } +} +$.ajaxSetup({ cache: false }); +setInterval("OpenLP.pollServer();", 500); +OpenLP.pollServer(); + diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index eedc30102..7a7acab2b 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -265,9 +265,11 @@ class HttpRouter(object): self.routes = [ (u'^/$', self.serve_file), (u'^/(stage)$', self.serve_file), + (u'^/(live)$', self.serve_file), (r'^/files/(.*)$', self.serve_file), (r'^/api/poll$', self.poll), (r'^/stage/poll$', self.poll), + (r'^/live/poll$', self.live_poll), (r'^/api/controller/(live|preview)/(.*)$', self.controller), (r'^/stage/controller/(live|preview)/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), @@ -305,6 +307,7 @@ class HttpRouter(object): if response: return response else: + log.debug('Path not found %s', url_path) return self._http_not_found() def _get_service_items(self): @@ -334,6 +337,7 @@ class HttpRouter(object): self.template_vars = { 'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'), 'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'), + 'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'), 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'), 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'), 'alerts': translate('RemotePlugin.Mobile', 'Alerts'), @@ -371,6 +375,8 @@ class HttpRouter(object): filename = u'index.html' elif filename == u'stage': filename = u'stage.html' + elif filename == u'live': + filename = u'live.html' path = os.path.normpath(os.path.join(self.html_dir, filename)) if not path.startswith(self.html_dir): return self._http_not_found() @@ -425,6 +431,16 @@ class HttpRouter(object): cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': result}) + def live_poll(self): + """ + Poll OpenLP to determine the current display value. + """ + result = { + u'slide_count': self.live_controller.slide_count + } + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) + def display(self, action): """ Hide or show the display screen. From e1f3024ff1e2965e770a7cd68f6aea942e0efab1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 21 May 2013 18:45:33 +0100 Subject: [PATCH 210/235] Latest version --- .../presentations/lib/messagelistener.py | 1 + openlp/plugins/remotes/html/live.html | 14 ++++-- openlp/plugins/remotes/html/live.js | 46 ++----------------- openlp/plugins/remotes/lib/httpserver.py | 38 ++++++++++++--- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 330c36f5c..219cae9a6 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -36,6 +36,7 @@ from openlp.core.ui import HideMode 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 diff --git a/openlp/plugins/remotes/html/live.html b/openlp/plugins/remotes/html/live.html index 2f505f98e..33a14fd52 100644 --- a/openlp/plugins/remotes/html/live.html +++ b/openlp/plugins/remotes/html/live.html @@ -29,14 +29,22 @@ --> - ${live_title} + ${live_title}A + - -
A
+ \ No newline at end of file diff --git a/openlp/plugins/remotes/html/live.js b/openlp/plugins/remotes/html/live.js index a44ce02ca..d55072c16 100644 --- a/openlp/plugins/remotes/html/live.js +++ b/openlp/plugins/remotes/html/live.js @@ -28,49 +28,9 @@ window.OpenLP = { $.getJSON( "/live/image", function (data, status) { - OpenLP.currentSlides = data.results.slides; - OpenLP.currentSlide = 0; - OpenLP.currentTags = Array(); - var div = $("#verseorder"); - div.html(""); - var tag = ""; - var tags = 0; - var lastChange = 0; - $.each(data.results.slides, function(idx, slide) { - var prevtag = tag; - tag = slide["tag"]; - if (tag != prevtag) { - // If the tag has changed, add new one to the list - lastChange = idx; - tags = tags + 1; - div.append(" "); - $("#verseorder span").last().attr("id", "tag" + tags).text(tag); - } - else { - if ((slide["text"] == data.results.slides[lastChange]["text"]) && - (data.results.slides.length > idx + (idx - lastChange))) { - // If the tag hasn't changed, check to see if the same verse - // has been repeated consecutively. Note the verse may have been - // split over several slides, so search through. If so, repeat the tag. - var match = true; - for (var idx2 = 0; idx2 < idx - lastChange; idx2++) { - if(data.results.slides[lastChange + idx2]["text"] != data.results.slides[idx + idx2]["text"]) { - match = false; - break; - } - } - if (match) { - lastChange = idx; - tags = tags + 1; - div.append(" "); - $("#verseorder span").last().attr("id", "tag" + tags).text(tag); - } - } - } - OpenLP.currentTags[idx] = tags; - if (slide["selected"]) - OpenLP.currentSlide = idx; - }) + var img = document.getElementById('image'); + img.src = data.results.slide_image; + img.style.display = 'block'; } ); }, diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 7a7acab2b..8d4872cfc 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -124,7 +124,7 @@ import cherrypy from mako.template import Template from PyQt4 import QtCore -from openlp.core.lib import Registry, Settings, PluginStatus, StringContent +from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte from openlp.core.utils import AppLocation, translate from cherrypy._cpcompat import sha, ntob @@ -175,9 +175,11 @@ class HttpServer(object): self.root = self.Public() self.root.files = self.Files() self.root.stage = self.Stage() + self.root.live = self.Live() self.root.router = self.router self.root.files.router = self.router self.root.stage.router = self.router + self.root.live.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 @@ -212,6 +214,9 @@ class HttpServer(object): 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}, + u'/live': {u'tools.staticdir.on': True, u'tools.staticdir.dir': self.router.html_dir, u'tools.basic_auth.on': False}} return directory_config @@ -239,7 +244,16 @@ class HttpServer(object): class Stage(object): """ - Stageview is read only so security is not relevant and would reduce it's usability + Stage view is read only so security is not relevant and would reduce it's usability + """ + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Live(object): + """ + Live view is read only so security is not relevant and would reduce it's usability """ @cherrypy.expose def default(self, *args, **kwargs): @@ -270,6 +284,7 @@ class HttpRouter(object): (r'^/api/poll$', self.poll), (r'^/stage/poll$', self.poll), (r'^/live/poll$', self.live_poll), + (r'^/live/image$', self.live_image), (r'^/api/controller/(live|preview)/(.*)$', self.controller), (r'^/stage/controller/(live|preview)/(.*)$', self.controller), (r'^/api/service/(.*)$', self.service), @@ -363,12 +378,11 @@ class HttpRouter(object): def serve_file(self, filename=None): """ - Send a file to the socket. For now, just a subset of file types - and must be top level inside the html folder. + Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder. If subfolders requested return 404, easier for security for the present. - 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' + 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' """ log.debug(u'serve file request %s' % filename) if not filename: @@ -433,7 +447,7 @@ class HttpRouter(object): def live_poll(self): """ - Poll OpenLP to determine the current display value. + Poll OpenLP to determine the current slide count. """ result = { u'slide_count': self.live_controller.slide_count @@ -441,6 +455,16 @@ class HttpRouter(object): cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': result}) + def live_image(self): + """ + Return the latest display image as a byte stream. + """ + result = { + u'slide_image': str(image_to_byte(self.live_controller.slide_image)) + } + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) + def display(self, action): """ Hide or show the display screen. From f22254501aa0f975cdf65530614e57e247ecc529 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Thu, 23 May 2013 09:31:12 +0200 Subject: [PATCH 211/235] Reverted another change from the 'adding multiple service items' functionality --- openlp/core/lib/mediamanageritem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index 2fde4a821..c3e1fa366 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -466,7 +466,7 @@ class MediaManagerItem(QtGui.QWidget): translate('OpenLP.MediaManagerItem', 'You must select one or more items to preview.')) else: log.debug(u'%s Preview requested', self.plugin.name) - service_item = self.build_service_item(context=ServiceItemContext.Preview) + service_item = self.build_service_item() if service_item: service_item.from_plugin = True self.preview_controller.add_service_item(service_item) From 3961db8b67822df688dd8732b08e6f2221b9afc6 Mon Sep 17 00:00:00 2001 From: "Peter S. Bentley" Date: Thu, 23 May 2013 17:15:15 +0100 Subject: [PATCH 212/235] Fix invalid object names --- openlp/plugins/songs/lib/mediaitem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 5d0bc086c..b60c0b162 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -132,8 +132,8 @@ class SongMediaItem(MediaManagerItem): """ Initialise variables when they cannot be initialised in the constructor. """ - self.songMaintenanceForm = SongMaintenanceForm(self.plugin.manager, self) - self.editSongForm = EditSongForm(self, self.main_window, self.plugin.manager) + self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self) + self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager) self.openLyrics = OpenLyrics(self.plugin.manager) self.search_text_edit.set_search_types([ (SongSearch.Entire, u':/songs/song_search_all.png', From 4b2868b27173df83c9a0edc48f3070714e09598e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 24 May 2013 21:17:47 +0100 Subject: [PATCH 213/235] Fox presentations and refactor service item --- openlp/core/lib/serviceitem.py | 35 ++++++++++--------- openlp/core/ui/media/mediacontroller.py | 4 +-- openlp/plugins/media/lib/mediaitem.py | 11 +++--- openlp/plugins/presentations/lib/mediaitem.py | 18 +++++----- .../presentations/lib/messagelistener.py | 3 +- .../lib/presentationcontroller.py | 5 +-- .../presentations/presentationplugin.py | 2 +- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index c4ac846c9..b32e1aaf0 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -58,8 +58,7 @@ class ItemCapabilities(object): Provides an enumeration of a service item's capabilities ``CanPreview`` - The capability to allow the ServiceManager to add to the preview - tab when making the previous item live. + The capability to allow the ServiceManager to add to the preview tab when making the previous item live. ``CanEdit`` The capability to allow the ServiceManager to allow the item to be edited @@ -71,8 +70,7 @@ class ItemCapabilities(object): Determines is the service_item needs a Media Player ``CanLoop`` - The capability to allow the SlideController to allow the loop - processing. + The capability to allow the SlideController to allow the loop processing. ``CanAppend`` The capability to allow the ServiceManager to add leaves to the @@ -82,22 +80,19 @@ class ItemCapabilities(object): The capability to remove lines breaks in the renderer ``OnLoadUpdate`` - The capability to update MediaManager when a service Item is - loaded. + The capability to update MediaManager when a service Item is loaded. ``AddIfNewItem`` Not Used ``ProvidesOwnDisplay`` - The capability to tell the SlideController the service Item has a - different display. + The capability to tell the SlideController the service Item has a different display. ``HasDetailedTitleDisplay`` - ServiceItem provides a title + Being Removed and decommissioned. ``HasVariableStartTime`` - The capability to tell the ServiceManager that a change to start - time is possible. + The capability to tell the ServiceManager that a change to start time is possible. ``CanSoftBreak`` The capability to tell the renderer that Soft Break is allowed @@ -149,7 +144,7 @@ class ServiceItem(object): if plugin: self.name = plugin.name self.title = u'' - self.shortname = u'' + self.processor = None self.audit = u'' self.items = [] self.iconic_representation = None @@ -353,7 +348,8 @@ class ServiceItem(object): u'media_length': self.media_length, u'background_audio': self.background_audio, u'theme_overwritten': self.theme_overwritten, - u'will_auto_start': self.will_auto_start + u'will_auto_start': self.will_auto_start, + u'processor': self.processor } service_data = [] if self.service_item_type == ServiceItemType.Text: @@ -387,7 +383,6 @@ class ServiceItem(object): self.title = header[u'title'] self.name = header[u'name'] self.service_item_type = header[u'type'] - self.shortname = header[u'plugin'] self.theme = header[u'theme'] self.add_icon(header[u'icon']) self.raw_footer = header[u'footer'] @@ -406,7 +401,13 @@ class ServiceItem(object): self.auto_play_slides_loop = header.get(u'auto_play_slides_loop', False) self.timed_slide_interval = header.get(u'timed_slide_interval', 0) self.will_auto_start = header.get(u'will_auto_start', False) + self.processor = header.get(u'processor', None) self.has_original_files = True + #TODO Remove me in 2,3 build phase + if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay): + self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay) + self.processor = self.title + self.title = None if u'background_audio' in header: self.background_audio = [] for filename in header[u'background_audio']: @@ -429,6 +430,8 @@ class ServiceItem(object): self.add_from_image(text_image[u'path'], text_image[u'title'], background) elif self.service_item_type == ServiceItemType.Command: for text_image in serviceitem[u'serviceitem'][u'data']: + if not self.title: + self.title = text_image[u'title'] if path: self.has_original_files = False self.add_from_command(path, text_image[u'title'], text_image[u'image']) @@ -443,9 +446,7 @@ class ServiceItem(object): if self.is_text(): return self.title else: - if ItemCapabilities.HasDetailedTitleDisplay in self.capabilities: - return self._raw_frames[0][u'title'] - elif len(self._raw_frames) > 1: + if len(self._raw_frames) > 1: return self.title else: return self._raw_frames[0][u'title'] diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 1e011a84d..71f2b4b10 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -466,8 +466,8 @@ class MediaController(object): The ServiceItem containing the details to be played. """ used_players = get_media_players()[0] - if service_item.title != UiStrings().Automatic: - used_players = [service_item.title.lower()] + if service_item.processor != UiStrings().Automatic: + used_players = [service_item.processor.lower()] if controller.media_info.file_info.isFile(): suffix = u'*.%s' % controller.media_info.file_info.suffix().lower() for title in used_players: diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 243fcd158..2f07110fe 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -155,7 +155,7 @@ class MediaMediaItem(MediaManagerItem): if os.path.exists(filename): service_item = ServiceItem() service_item.title = u'webkit' - service_item.shortname = service_item.title + service_item.processor = u'webkit' (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): @@ -185,9 +185,9 @@ 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.display_type_combo_box.currentText() - service_item.shortname = service_item.title (path, name) = os.path.split(filename) + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() service_item.add_from_command(path, name, CLAPPERBOARD) # Only get start and end times if going to a service if context == ServiceItemContext.Service: @@ -196,7 +196,6 @@ class MediaMediaItem(MediaManagerItem): return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.RequiresMedia) - service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay) if Settings().value(self.settings_section + u'/media auto start') == QtCore.Qt.Checked: service_item.will_auto_start = True # force a non-existent theme @@ -208,6 +207,7 @@ class MediaMediaItem(MediaManagerItem): self.list_view.setIconSize(QtCore.QSize(88, 50)) self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settings_section), u'thumbnails') check_directory_exists(self.servicePath) + print self.settings_section + u'/media files' self.load_list(Settings().value(self.settings_section + u'/media files')) self.populateDisplayTypes() @@ -260,8 +260,7 @@ class MediaMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + u'/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific - # characters. + # Sort the media by its filename considering language specific characters. media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1])) for track in media: track_info = QtCore.QFileInfo(track) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 2f48b99c1..fcfc495ed 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -244,22 +244,20 @@ class PresentationMediaItem(MediaManagerItem): items = self.list_view.selectedItems() if len(items) > 1: return False - service_item.title = self.display_type_combo_box.currentText() - service_item.shortname = self.display_type_combo_box.currentText() + service_item.processor = self.display_type_combo_box.currentText() service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) - service_item.add_capability(ItemCapabilities.HasDetailedTitleDisplay) - shortname = service_item.shortname - if not shortname: + if not self.display_type_combo_box.currentText(): return False for bitem in items: filename = bitem.data(QtCore.Qt.UserRole) + (path, name) = os.path.split(filename) + service_item.title = name if os.path.exists(filename): - if shortname == self.Automatic: - service_item.shortname = self.findControllerByType(filename) - if not service_item.shortname: + if service_item.processor == self.Automatic: + service_item.processor = self.findControllerByType(filename) + if not service_item.processor: return False - controller = self.controllers[service_item.shortname] - (path, name) = os.path.split(filename) + controller = self.controllers[service_item.processor] doc = controller.add_document(filename) if doc.get_thumbnail_path(1, True) is None: doc.load_presentation() diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 330c36f5c..cd7c654a2 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -36,6 +36,7 @@ from openlp.core.ui import HideMode 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 @@ -314,7 +315,7 @@ class MessageListener(object): item = message[0] hide_mode = message[2] file = item.get_frame_path() - self.handler = item.title + self.handler = item.processor if self.handler == self.media_item.Automatic: self.handler = self.media_item.findControllerByType(file) if not self.handler: diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 7501fd6df..85ebc42ad 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -38,6 +38,7 @@ from openlp.core.utils import AppLocation 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 @@ -322,7 +323,7 @@ class PresentationController(object): ``supports`` The primary native file types this application supports. - ``alsosupports`` + ``also_supports`` Other file types the application can import, although not necessarily the first choice due to potential incompatibilities. @@ -358,7 +359,7 @@ class PresentationController(object): Name of the application, to appear in the application """ self.supports = [] - self.alsosupports = [] + self.also_supports = [] self.docs = [] self.plugin = plugin self.name = name diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 1cb966aa5..cc1516b69 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -49,7 +49,7 @@ __default_settings__ = { u'presentations/Powerpoint': QtCore.Qt.Checked, u'presentations/Powerpoint Viewer': QtCore.Qt.Checked, u'presentations/presentations files': [] - } +} class PresentationPlugin(Plugin): From 6dd070f60ecca6356593a4412463535fdb18e1a3 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 25 May 2013 06:48:08 +0100 Subject: [PATCH 214/235] Add migration test --- .../openlp_core_lib/test_serviceitem.py | 21 +++- tests/resources/migrate_video_20_22.osd | 98 +++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/resources/migrate_video_20_22.osd diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index 26e9e7d44..e051f8c76 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -210,7 +210,6 @@ class TestServiceItem(TestCase): # THEN: We should get back a valid service item assert service_item.is_valid is True, u'The new service item should be valid' - print service_item.get_rendered_frame(0) assert service_item.get_rendered_frame(0) == test_file, u'The first frame should match the path to the image' assert service_item.get_frames()[0] == frame_array, u'The return should match frame array1' assert service_item.get_frame_path(0) == test_file, u'The frame path should match the full path to the image' @@ -268,6 +267,26 @@ class TestServiceItem(TestCase): assert service_item.is_capable(ItemCapabilities.CanAppend) is True, \ u'This service item should be able to have new items added to it' + def serviceitem_migrate_test_20_22(self): + """ + Test the Service Item - migrating a media only service item from 2.0 to 2.2 format + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: adding an media from a saved Service and mocked exists + line = self.convert_file_service_item(u'migrate_video_20_22.osd') + with patch('os.path.exists'): + service_item.set_from_service(line, TEST_PATH) + + # THEN: We should get back a converted service item + assert service_item.is_valid is True, u'The new service item should be valid' + assert service_item.processor is None, u'The Processor should have been set' + assert service_item.title is None, u'The title should be set to a value' + assert service_item.is_capable(ItemCapabilities.HasDetailedTitleDisplay) is False, \ + u'The Capability should have been removed' + def convert_file_service_item(self, name): service_file = os.path.join(TEST_PATH, name) try: diff --git a/tests/resources/migrate_video_20_22.osd b/tests/resources/migrate_video_20_22.osd new file mode 100644 index 000000000..ee4b2c0c4 --- /dev/null +++ b/tests/resources/migrate_video_20_22.osd @@ -0,0 +1,98 @@ +(lp1 +(dp2 +Vserviceitem +p3 +(dp4 +Vheader +p5 +(dp6 +Vxml_version +p7 +NsVauto_play_slides_loop +p8 +I00 +sVauto_play_slides_once +p9 +I00 +sVwill_auto_start +p10 +I01 +sVtitle +p11 +VVLC +p12 +sVcapabilities +p13 +(lp14 +I12 +aI16 +aI4 +aI11 +asVtheme +p15 +I-1 +sVbackground_audio +p16 +(lp17 +sVicon +p18 +V:/plugins/plugin_media.png +p19 +sVtype +p20 +I3 +sVstart_time +p21 +I0 +sVfrom_plugin +p22 +I00 +sVmedia_length +p23 +I144 +sVdata +p24 +V +sVtimed_slide_interval +p25 +I0 +sVaudit +p26 +V +sVsearch +p27 +V +sVname +p28 +Vmedia +p29 +sVfooter +p30 +(lp31 +sVnotes +p32 +V +sVplugin +p33 +g29 +sVtheme_overwritten +p34 +I00 +sVend_time +p35 +I0 +ssg24 +(lp36 +(dp37 +Vpath +p38 +V/home/tim/Videos/puppets +p39 +sVimage +p40 +V:/media/slidecontroller_multimedia.png +p41 +sg11 +VMVI_3405.MOV +p42 +sassa. \ No newline at end of file From 0a8db629d9ecb7f50ca7df07af4694817354ae74 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 25 May 2013 06:54:25 +0100 Subject: [PATCH 215/235] Remove Print --- openlp/plugins/media/lib/mediaitem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 2f07110fe..30748a225 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -207,7 +207,6 @@ class MediaMediaItem(MediaManagerItem): self.list_view.setIconSize(QtCore.QSize(88, 50)) self.servicePath = os.path.join(AppLocation.get_section_data_path(self.settings_section), u'thumbnails') check_directory_exists(self.servicePath) - print self.settings_section + u'/media files' self.load_list(Settings().value(self.settings_section + u'/media files')) self.populateDisplayTypes() From 20bd86fbce5d60f03b0b7d300b93f29c4adc174f Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 25 May 2013 18:05:44 +0100 Subject: [PATCH 216/235] Live works! --- openlp/core/lib/__init__.py | 3 ++- openlp/plugins/remotes/html/live.html | 2 ++ openlp/plugins/remotes/lib/httpserver.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index d6c338271..6bd20f2fd 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -183,7 +183,8 @@ def image_to_byte(image): # use buffer to store pixmap into byteArray buffie = QtCore.QBuffer(byte_array) buffie.open(QtCore.QIODevice.WriteOnly) - image.save(buffie, "PNG") + if isinstance(image, QtGui.QImage): + image.save(buffie, "PNG") log.debug(u'image_to_byte - end') # convert to base64 encoding so does not get missed! return byte_array.toBase64() diff --git a/openlp/plugins/remotes/html/live.html b/openlp/plugins/remotes/html/live.html index 33a14fd52..fed8f6c98 100644 --- a/openlp/plugins/remotes/html/live.html +++ b/openlp/plugins/remotes/html/live.html @@ -42,6 +42,8 @@ top: 0px; width: 100%%; height: 100%%; + background-size: cover; + background-repeat: no-repeat; } diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 8d4872cfc..a2abbb41e 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -136,6 +136,7 @@ def make_sha_hash(password): """ Create an encrypted password for the given password. """ + log.debug("make_sha_hash") return sha(ntob(password)).hexdigest() @@ -143,6 +144,7 @@ def fetch_password(username): """ Fetch the password for a provided user. """ + log.debug("Fetch Password") if username != Settings().value(u'remotes/user id'): return None return make_sha_hash(Settings().value(u'remotes/password')) @@ -460,7 +462,7 @@ class HttpRouter(object): Return the latest display image as a byte stream. """ result = { - u'slide_image': str(image_to_byte(self.live_controller.slide_image)) + u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image)) } cherrypy.response.headers['Content-Type'] = u'application/json' return json.dumps({u'results': result}) From 512155ff68b54052691df9e8cf704f25ce3844b7 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 3 Jun 2013 18:19:42 +0100 Subject: [PATCH 217/235] Add css file --- openlp/core/lib/__init__.py | 3 +-- openlp/plugins/remotes/html/live.css | 39 +++++++++++++++++++++++++++ openlp/plugins/remotes/html/live.html | 15 ++--------- 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 openlp/plugins/remotes/html/live.css diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index 6bd20f2fd..d6c338271 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -183,8 +183,7 @@ def image_to_byte(image): # use buffer to store pixmap into byteArray buffie = QtCore.QBuffer(byte_array) buffie.open(QtCore.QIODevice.WriteOnly) - if isinstance(image, QtGui.QImage): - image.save(buffie, "PNG") + image.save(buffie, "PNG") log.debug(u'image_to_byte - end') # convert to base64 encoding so does not get missed! return byte_array.toBase64() diff --git a/openlp/plugins/remotes/html/live.css b/openlp/plugins/remotes/html/live.css new file mode 100644 index 000000000..7181129d9 --- /dev/null +++ b/openlp/plugins/remotes/html/live.css @@ -0,0 +1,39 @@ +/****************************************************************************** +* 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 * +******************************************************************************/ +body { + background-color: black; + font-family: sans-serif; + overflow: hidden; +} + +.size { + position: absolute; + top: 0px; + vertical-align: middle; + height: 100%; + background-size: cover; + background-repeat: no-repeat; +} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/live.html b/openlp/plugins/remotes/html/live.html index fed8f6c98..f9a2c874c 100644 --- a/openlp/plugins/remotes/html/live.html +++ b/openlp/plugins/remotes/html/live.html @@ -29,23 +29,12 @@ --> - ${live_title}A - + ${live_title} + - From 9160832e0a1e3b4e292e8caafd0f65d938c28fc5 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 3 Jun 2013 19:03:45 +0100 Subject: [PATCH 218/235] fix error --- openlp/core/ui/slidecontroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index e2d318549..51ac4f525 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -121,6 +121,8 @@ class SlideController(DisplayController): self.update_slide_limits() self.panel = QtGui.QWidget(parent.controlSplitter) self.slideList = {} + self.slide_count = 0 + self.slide_image = None # Layout for holding panel self.panel_layout = QtGui.QVBoxLayout(self.panel) self.panel_layout.setSpacing(0) @@ -136,8 +138,6 @@ class SlideController(DisplayController): self.keypress_loop = False self.category = UiStrings().LiveToolbar ActionList.get_instance().add_category(unicode(self.category), CategoryOrder.standard_toolbar) - self.slide_count = 0 - self.slide_image = None else: Registry().register(u'preview_controller', self) self.type_label.setText(UiStrings().Preview) From 6e5588b7cfc5d2787d4a05ff88715b1b2c5f683a Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 3 Jun 2013 19:47:25 +0100 Subject: [PATCH 219/235] Update settings and minor cleanups --- openlp/core/ui/mainwindow.py | 8 +-- openlp/core/ui/slidecontroller.py | 81 ++++++++++++------------- openlp/plugins/remotes/lib/remotetab.py | 26 ++++++-- 3 files changed, 65 insertions(+), 50 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 2afbb4eb0..6b86bfe7a 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -779,8 +779,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): """ We need to make sure, that the SlidePreview's size is correct. """ - self.preview_controller.previewSizeChanged() - self.live_controller.previewSizeChanged() + self.preview_controller.preview_size_changed() + self.live_controller.preview_size_changed() def on_settings_shortcuts_item_clicked(self): """ @@ -989,8 +989,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.application.set_busy_cursor() self.image_manager.update_display() self.renderer.update_display() - self.preview_controller.screenSizeChanged() - self.live_controller.screenSizeChanged() + self.preview_controller.screen_size_changed() + self.live_controller.screen_size_changed() self.setFocus() self.activateWindow() self.application.set_normal_cursor() diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 51ac4f525..febef0850 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -89,7 +89,7 @@ class SlideController(DisplayController): Set up the Slide Controller. """ DisplayController.__init__(self, parent, is_live) - Registry().register_function(u'bootstrap_post_set_up', self.screenSizeChanged) + Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed) self.screens = ScreenList() try: self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height()) @@ -323,18 +323,18 @@ class SlideController(DisplayController): self.slide_layout.insertWidget(0, self.preview_display) self.preview_display.hide() # Actual preview screen - self.slidePreview = QtGui.QLabel(self) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.slidePreview.sizePolicy().hasHeightForWidth()) - self.slidePreview.setSizePolicy(sizePolicy) - self.slidePreview.setFrameShape(QtGui.QFrame.Box) - self.slidePreview.setFrameShadow(QtGui.QFrame.Plain) - self.slidePreview.setLineWidth(1) - self.slidePreview.setScaledContents(True) - self.slidePreview.setObjectName(u'slidePreview') - self.slide_layout.insertWidget(0, self.slidePreview) + self.slide_preview = QtGui.QLabel(self) + size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth()) + self.slide_preview.setSizePolicy(size_policy) + self.slide_preview.setFrameShape(QtGui.QFrame.Box) + self.slide_preview.setFrameShadow(QtGui.QFrame.Plain) + self.slide_preview.setLineWidth(1) + self.slide_preview.setScaledContents(True) + self.slide_preview.setObjectName(u'slide_preview') + self.slide_layout.insertWidget(0, self.slide_preview) self.grid.addLayout(self.slide_layout, 0, 0, 1, 1) if self.is_live: self.current_shortcut = u'' @@ -519,10 +519,9 @@ class SlideController(DisplayController): self.service_manager.next_item() self.keypress_loop = False - def screenSizeChanged(self): + def screen_size_changed(self): """ - Settings dialog has changed the screen size of adjust output and - screen previews. + Settings dialog has changed the screen size of adjust output and screen previews. """ # rebuild display as screen size changed if self.display: @@ -538,14 +537,14 @@ class SlideController(DisplayController): except ZeroDivisionError: self.ratio = 1 self.media_controller.setup_display(self.display, False) - self.previewSizeChanged() + self.preview_size_changed() self.preview_display.setup() service_item = ServiceItem() self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live, plugins=self.plugin_manager.plugins)) self.media_controller.setup_display(self.preview_display, True) if self.service_item: - self.refreshServiceItem() + self.refresh_service_item() def __addActionsToWidget(self, widget): """ @@ -556,7 +555,7 @@ class SlideController(DisplayController): self.previousService, self.nextService, self.escapeItem]) - def previewSizeChanged(self): + def preview_size_changed(self): """ Takes care of the SlidePreview's size. Is called when one of the the splitters is moved or when the screen size is changed. Note, that this @@ -565,14 +564,14 @@ class SlideController(DisplayController): if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()): # We have to take the height as limit. max_height = self.preview_frame.height() - self.grid.margin() * 2 - self.slidePreview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) + self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.preview_display.screen = { u'size': self.preview_display.geometry()} else: # We have to take the width as limit. max_width = self.preview_frame.width() - self.grid.margin() * 2 - self.slidePreview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) + self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.screen = { u'size': self.preview_display.geometry()} @@ -626,17 +625,16 @@ class SlideController(DisplayController): """ self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits') - def enableToolBar(self, item): + def enable_tool_bar(self, item): """ - Allows the toolbars to be reconfigured based on Controller Type - and ServiceItem Type + Allows the toolbars to be reconfigured based on Controller Type and ServiceItem Type """ if self.is_live: - self.enableLiveToolBar(item) + self.enable_live_tool_bar(item) else: - self.enablePreviewToolBar(item) + self.enable_preview_tool_bar(item) - def enableLiveToolBar(self, item): + def enable_live_tool_bar(self, item): """ Allows the live toolbar to be customised """ @@ -665,7 +663,7 @@ class SlideController(DisplayController): # See bug #791050 self.toolbar.show() - def enablePreviewToolBar(self, item): + def enable_preview_tool_bar(self, item): """ Allows the Preview toolbar to be customised """ @@ -684,15 +682,15 @@ class SlideController(DisplayController): # See bug #791050 self.toolbar.show() - def refreshServiceItem(self): + def refresh_service_item(self): """ Method to update the service item if the screen has changed """ - log.debug(u'refreshServiceItem live = %s' % self.is_live) + log.debug(u'refresh_service_item live = %s' % self.is_live) if self.service_item.is_text() or self.service_item.is_image(): item = self.service_item item.render() - self._processItem(item, self.selected_row) + self._process_item(item, self.selected_row) def add_service_item(self, item): """ @@ -705,14 +703,14 @@ class SlideController(DisplayController): if self.song_edit: slideno = self.selected_row self.song_edit = False - self._processItem(item, slideno) + self._process_item(item, slideno) def replaceServiceManagerItem(self, item): """ Replacement item following a remote edit """ if item == self.service_item: - self._processItem(item, self.preview_list_widget.currentRow()) + self._process_item(item, self.preview_list_widget.currentRow()) def addServiceManagerItem(self, item, slideno): """ @@ -731,7 +729,7 @@ class SlideController(DisplayController): self.__checkUpdateSelectedSlide(slidenum) self.slideSelected() else: - self._processItem(item, slidenum) + self._process_item(item, slidenum) if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0: self.play_slides_loop.setChecked(item.auto_play_slides_loop) self.delay_spin_box.setValue(int(item.timed_slide_interval)) @@ -741,7 +739,7 @@ class SlideController(DisplayController): self.delay_spin_box.setValue(int(item.timed_slide_interval)) self.onPlaySlidesOnce() - def _processItem(self, service_item, slideno): + def _process_item(self, service_item, slideno): """ Loads a ServiceItem into the system from ServiceManager Display the slide number passed @@ -829,10 +827,9 @@ class SlideController(DisplayController): self.preview_list_widget.setVerticalHeaderLabels(text) if self.service_item.is_text(): self.preview_list_widget.resizeRowsToContents() - self.preview_list_widget.setColumnWidth(0, - self.preview_list_widget.viewport().size().width()) + self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width()) self.__updatePreviewSelection(slideno) - self.enableToolBar(service_item) + self.enable_tool_bar(service_item) # Pass to display for viewing. # Postpone image build, we need to do this later to avoid the theme # flashing on the screen @@ -1062,7 +1059,7 @@ class SlideController(DisplayController): QtCore.QTimer.singleShot(2.5, self.grab_maindisplay) else: self.slide_image = self.display.preview() - self.slidePreview.setPixmap(self.slide_image) + self.slide_preview.setPixmap(self.slide_image) self.slide_count += 1 def grab_maindisplay(self): @@ -1072,7 +1069,7 @@ class SlideController(DisplayController): win_id = QtGui.QApplication.desktop().winId() rect = self.screens.current[u'size'] win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height()) - self.slidePreview.setPixmap(win_image) + self.slide_preview.setPixmap(win_image) self.slide_image = win_image def on_slide_selected_next_action(self, checked): @@ -1279,7 +1276,7 @@ class SlideController(DisplayController): self.media_controller.video(self.controller_type, item, self.hide_mode()) if not self.is_live: self.preview_display.show() - self.slidePreview.hide() + self.slide_preview.hide() def onMediaClose(self): """ @@ -1288,7 +1285,7 @@ class SlideController(DisplayController): log.debug(u'SlideController onMediaClose') self.media_controller.media_reset(self) self.preview_display.hide() - self.slidePreview.show() + self.slide_preview.show() def _resetBlank(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 09934b58c..c8ed9303e 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -86,6 +86,12 @@ class RemoteTab(SettingsTab): 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.live_url_label = QtGui.QLabel(self.http_settings_group_box) + self.live_url_label.setObjectName(u'live_url_label') + self.live_url = QtGui.QLabel(self.http_settings_group_box) + self.live_url.setObjectName(u'live_url') + self.live_url.setOpenExternalLinks(True) + self.http_setting_layout.addRow(self.live_url_label, self.live_url) self.left_layout.addWidget(self.http_settings_group_box) self.https_settings_group_box = QtGui.QGroupBox(self.left_column) self.https_settings_group_box.setCheckable(True) @@ -116,6 +122,12 @@ class RemoteTab(SettingsTab): 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.live_https_url_label = QtGui.QLabel(self.https_settings_group_box) + self.live_https_url_label.setObjectName(u'live_url_label') + self.live_https_url = QtGui.QLabel(self.https_settings_group_box) + self.live_https_url.setObjectName(u'live_https_url') + self.live_https_url.setOpenExternalLinks(True) + self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url) self.left_layout.addWidget(self.https_settings_group_box) self.user_login_group_box = QtGui.QGroupBox(self.left_column) self.user_login_group_box.setCheckable(True) @@ -163,6 +175,7 @@ class RemoteTab(SettingsTab): 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.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.qr_description_label.setText(translate('RemotePlugin.RemoteTab', @@ -176,6 +189,7 @@ class RemoteTab(SettingsTab): 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.live_https_url_label.setText(self.live_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:')) @@ -203,10 +217,14 @@ class RemoteTab(SettingsTab): https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value()) self.remote_url.setText(u'%s' % (http_url, http_url)) self.remote_https_url.setText(u'%s' % (https_url, https_url)) - http_url += u'stage' - https_url += u'stage' - self.stage_url.setText(u'%s' % (http_url, http_url)) - self.stage_https_url.setText(u'%s' % (https_url, https_url)) + http_url_temp = http_url + u'stage' + https_url_temp = https_url + u'stage' + self.stage_url.setText(u'%s' % (http_url_temp, http_url_temp)) + self.stage_https_url.setText(u'%s' % (https_url_temp, https_url_temp)) + http_url_temp = http_url + u'live' + https_url_temp = https_url + u'live' + self.live_url.setText(u'%s' % (http_url_temp, http_url_temp)) + self.live_https_url.setText(u'%s' % (https_url_temp, https_url_temp)) def load(self): """ From 43326c5781d2f829286961d37d3c37219e806006 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 6 Jun 2013 08:40:10 +0200 Subject: [PATCH 220/235] fixed code --- openlp/.version | 2 +- openlp/core/utils/__init__.py | 41 +++++++++++++++++++----------- setup.py | 47 ++++++++++++++++++++++------------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/openlp/.version b/openlp/.version index b6e0556c1..3245d0c77 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.0.1-bzr2233 \ No newline at end of file +2.1.0-bzr2234 diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index f1c150989..5c4a4466c 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -101,25 +101,38 @@ def get_application_version(): if APPLICATION_VERSION: return APPLICATION_VERSION if u'--dev-version' in sys.argv or u'-d' in sys.argv: - # If we're running the dev version, let's use bzr to get the version. - bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) + # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied + # there. + + # Get the revision of this tree. + bzr = Popen((u'bzr', u'revno'), stdout=PIPE) + tree_revision, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr log') + + # Get all tags. + bzr = Popen((u'bzr', u'tags'), stdout=PIPE) output, error = bzr.communicate() code = bzr.wait() if code != 0: raise Exception(u'Error running bzr tags') - lines = output.splitlines() - if not lines: - tag = u'0.0.0' - revision = u'0' + tags = output.splitlines() + if not tags: + tag_version = u'0.0.0' + tag_revision = u'0' else: - tag, revision = lines[-1].split() - bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr log') - latest = output.split(u':')[0] - full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) + # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from + # another series. + tags = [tag for tag in tags if tag.split()[-1].strip() != u'?'] + # Get the last tag and split it in a revision and tag name. + tag_version, tag_revision = tags[-1].split() + # If they are equal, then this tree is tarball with the source for the release. We do not want the revision + # number in the full version. + if tree_revision == tag_revision: + full_version = tag_version + else: + full_version = u'%s-bzr%s' % (tag_version, tree_revision) else: # We're not running the development version, let's use the file. filepath = AppLocation.get_directory(AppLocation.VersionDir) diff --git a/setup.py b/setup.py index 3bd3007b7..c9a192591 100755 --- a/setup.py +++ b/setup.py @@ -82,37 +82,50 @@ def natural_sort(seq, compare=natural_compare): temp.sort(compare) return temp +# NOTE: The following code is a duplicate of the code in openlp/core/utils/__init__.py. Any fix applied here should also +# be applied there. try: - bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE) + # Get the revision of this tree. + bzr = Popen((u'bzr', u'revno'), stdout=PIPE) + tree_revision, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception(u'Error running bzr log') + + # Get all tags. + bzr = Popen((u'bzr', u'tags'), stdout=PIPE) output, error = bzr.communicate() code = bzr.wait() if code != 0: raise Exception(u'Error running bzr tags') - lines = output.splitlines() - if not lines: - tag = u'0.0.0' - revision = u'0' + tags = output.splitlines() + if not tags: + tag_version = u'0.0.0' + tag_revision = u'0' else: - tag, revision = lines[-1].split() - bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception(u'Error running bzr log') - latest = output.split(u':')[0] - version = latest == revision and tag or u'%s-bzr%s' % (tag, latest) + # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from + # another series. + tags = [tag for tag in tags if tag.split()[-1].strip() != u'?'] + # Get the last tag and split it in a revision and tag name. + tag_version, tag_revision = tags[-1].split() + # If they are equal, then this tree is tarball with the source for the release. We do not want the revision number + # in the version string. + if tree_revision == tag_revision: + version_string = tag_version + else: + version_string = u'%s-bzr%s' % (tag_version, tree_revision) ver_file = open(VERSION_FILE, u'w') - ver_file.write(version) - ver_file.close() + ver_file.write(version_string) except: ver_file = open(VERSION_FILE, u'r') - version = ver_file.read().strip() + version_string = ver_file.read().strip() +finally: ver_file.close() setup( name='OpenLP', - version=version, + version=version_string, description="Open source Church presentation and lyrics projection application.", long_description="""\ OpenLP (previously openlp.org) is free church presentation software, or lyrics projection software, used to display slides of songs, Bible verses, videos, images, and even presentations (if PowerPoint is installed) for church worship using a computer and a data projector.""", From b172153b15f0a6681c393f3fb45cdce07f5ce786 Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Tue, 11 Jun 2013 22:27:30 +0200 Subject: [PATCH 221/235] Use future division. --- openlp/plugins/songs/forms/duplicatesongremovalform.py | 4 ++-- openlp/plugins/songs/lib/songcompare.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index ee27d77a6..5a01ede47 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -29,6 +29,7 @@ """ The duplicate song removal logic for OpenLP. """ +from __future__ import division import logging import os @@ -161,7 +162,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.notify_no_duplicates() return # With x songs we have x*(x - 1) / 2 comparisons. - max_progress_count = max_songs * (max_songs - 1) / 2 + max_progress_count = max_songs * (max_songs - 1) // 2 self.duplicate_search_progress_bar.setMaximum(max_progress_count) songs = self.plugin.manager.get_all_objects(Song) for outer_song_counter in range(max_songs - 1): @@ -194,7 +195,6 @@ class DuplicateSongRemovalForm(OpenLPWizard): translate(u'Wizard', u'No duplicate songs have been found in the database.'), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok)) - def add_duplicates_to_song_list(self, search_song, duplicate_song): """ Inserts a song duplicate (two similar songs) to the duplicate song list. diff --git a/openlp/plugins/songs/lib/songcompare.py b/openlp/plugins/songs/lib/songcompare.py index a98e61380..f69c5c827 100644 --- a/openlp/plugins/songs/lib/songcompare.py +++ b/openlp/plugins/songs/lib/songcompare.py @@ -43,6 +43,7 @@ Finally two conditions can qualify a song tuple to be a duplicate: This condition should hit if one of the two songs (or both) is small (smaller than the min_block_size), but most of the song is contained in the other song. """ +from __future__ import division import difflib @@ -85,7 +86,7 @@ def songs_probably_equal(song1, song2): for element in diff_no_typos: if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block: length_of_longest_equal_block = _op_length(element) - if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 / 3: + if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 // 3: return True # Both checks failed. We assume the songs are not equal. return False From 6f0ea093279d283b4da0b5aef05fb490988d0af1 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Fri, 14 Jun 2013 11:56:53 +0200 Subject: [PATCH 222/235] increased possible min/max line spacing Fixes: https://launchpad.net/bugs/645452 --- openlp/core/ui/themewizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 7fc78a59e..7c9f7df15 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -178,8 +178,8 @@ class Ui_ThemeWizard(object): self.lineSpacingLabel = QtGui.QLabel(self.mainAreaPage) self.lineSpacingLabel.setObjectName(u'LineSpacingLabel') self.lineSpacingSpinBox = QtGui.QSpinBox(self.mainAreaPage) - self.lineSpacingSpinBox.setMinimum(-50) - self.lineSpacingSpinBox.setMaximum(50) + self.lineSpacingSpinBox.setMinimum(-250) + self.lineSpacingSpinBox.setMaximum(250) self.lineSpacingSpinBox.setObjectName(u'LineSpacingSpinBox') self.mainAreaLayout.addRow(self.lineSpacingLabel, self.lineSpacingSpinBox) self.outlineCheckBox = QtGui.QCheckBox(self.mainAreaPage) From 2f55624977b3969c83ace5d67e6259a70eab974b Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Fri, 14 Jun 2013 22:20:26 +0200 Subject: [PATCH 223/235] Extract method to delete a song. --- .../songs/forms/duplicatesongremovalform.py | 19 ++--------- openlp/plugins/songs/lib/__init__.py | 34 ++++++++++++++++++- openlp/plugins/songs/lib/mediaitem.py | 16 ++------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 5a01ede47..640403adc 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -38,6 +38,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.lib import Registry, translate from openlp.core.ui.wizard import OpenLPWizard, WizardStrings from openlp.core.utils import AppLocation +from openlp.plugins.songs.lib import delete_song from openlp.plugins.songs.lib.db import Song, MediaFile from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget from openlp.plugins.songs.lib.songcompare import songs_probably_equal @@ -273,23 +274,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): # Remove song from duplicate song list. self.duplicate_song_list[-1].remove(song_review_widget.song) # Remove song from the database. - item_id = song_review_widget.song.id - media_files = self.plugin.manager.get_all_objects(MediaFile, - MediaFile.song_id == item_id) - for media_file in media_files: - try: - os.remove(media_file.file_name) - except: - log.exception(u'Could not remove file: %s', - media_file.file_name) - try: - save_path = os.path.join(AppLocation.get_section_data_path( - self.plugin.name), u'audio', str(item_id)) - if os.path.exists(save_path): - os.rmdir(save_path) - except OSError: - log.exception(u'Could not remove directory: %s', save_path) - self.plugin.manager.delete_object(Song, item_id) + delete_song(song_review_widget.song.id, self.plugin) # Remove GUI elements for the song. self.review_scroll_area_layout.removeWidget(song_review_widget) song_review_widget.setParent(None) diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index d3005c9b2..407fede8d 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -29,15 +29,21 @@ """ The :mod:`~openlp.plugins.songs.lib` module contains a number of library functions and classes used in the Songs plugin. """ + +import logging +import os import re from PyQt4 import QtGui from openlp.core.lib import translate -from openlp.core.utils import CONTROL_CHARS +from openlp.core.utils import AppLocation, CONTROL_CHARS +from openlp.plugins.songs.lib.db import MediaFile, Song from db import Author from ui import SongStrings +log = logging.getLogger(__name__) + WHITESPACE = re.compile(r'[\W_]+', re.UNICODE) APOSTROPHE = re.compile(u'[\'`’ʻ′]', re.UNICODE) PATTERN = re.compile(r"\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z])|([{}])|[\r\n]+|(.)", re.I) @@ -593,3 +599,29 @@ def strip_rtf(text, default_encoding=None): text = u''.join(out) return text, default_encoding + +def delete_song(song_id, song_plugin): + """ + Deletes a song from the database. Media files associated to the song + are removed prior to the deletion of the song. + + ``song_id`` + The ID of the song to delete. + + ``song_plugin`` + The song plugin instance. + """ + media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id) + for media_file in media_files: + try: + os.remove(media_file.file_name) + except: + log.exception('Could not remove file: %s', media_file.file_name) + try: + save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id)) + if os.path.exists(save_path): + os.rmdir(save_path) + except OSError: + log.exception(u'Could not remove directory: %s', save_path) + song_plugin.manager.delete_object(Song, song_id) + diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index b60c0b162..7c50eb6b8 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -43,7 +43,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songexportform import SongExportForm -from openlp.plugins.songs.lib import VerseType, clean_string +from openlp.plugins.songs.lib import VerseType, clean_string, delete_song 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 @@ -368,19 +368,7 @@ class SongMediaItem(MediaManagerItem): self.main_window.display_progress_bar(len(items)) for item in items: item_id = item.data(QtCore.Qt.UserRole) - media_files = self.plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == item_id) - for media_file in media_files: - try: - os.remove(media_file.file_name) - except: - log.exception('Could not remove file: %s', media_file.file_name) - try: - save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(item_id)) - if os.path.exists(save_path): - os.rmdir(save_path) - except OSError: - log.exception(u'Could not remove directory: %s', save_path) - self.plugin.manager.delete_object(Song, item_id) + delete_song(item_id, self.plugin) self.main_window.increment_progress_bar() self.main_window.finished_progress_bar() self.application.set_normal_cursor() From b18d96b3c9ce85ba0c6c4846e41284b84b7cacfa Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 15 Jun 2013 12:46:00 +0200 Subject: [PATCH 224/235] Keep GUI responsive during search. Allow aborting the search. --- .../songs/forms/duplicatesongremovalform.py | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py index 640403adc..d612a5627 100644 --- a/openlp/plugins/songs/forms/duplicatesongremovalform.py +++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py @@ -65,6 +65,8 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.duplicate_song_list = [] self.review_current_count = 0 self.review_total_count = 0 + # Used to interrupt ongoing searches when cancel is clicked. + self.break_search = False OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard', u':/wizards/wizard_duplicateremoval.bmp', False) self.setMinimumWidth(730) @@ -154,33 +156,39 @@ class DuplicateSongRemovalForm(OpenLPWizard): self.button(QtGui.QWizard.BackButton).hide() if page_id == self.searching_page_id: self.application.set_busy_cursor() - self.button(QtGui.QWizard.NextButton).hide() - # Search duplicate songs. - max_songs = self.plugin.manager.get_object_count(Song) - if max_songs == 0 or max_songs == 1: - self.duplicate_search_progress_bar.setMaximum(1) - self.duplicate_search_progress_bar.setValue(1) - self.notify_no_duplicates() - return - # With x songs we have x*(x - 1) / 2 comparisons. - max_progress_count = max_songs * (max_songs - 1) // 2 - self.duplicate_search_progress_bar.setMaximum(max_progress_count) - songs = self.plugin.manager.get_all_objects(Song) - for outer_song_counter in range(max_songs - 1): - for inner_song_counter in range(outer_song_counter + 1, max_songs): - if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): - duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter], - songs[inner_song_counter]) - if duplicate_added: - self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " + - songs[inner_song_counter].title) - self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) - self.review_total_count = len(self.duplicate_song_list) - if self.review_total_count == 0: - self.notify_no_duplicates() - else: - self.button(QtGui.QWizard.NextButton).show() - self.application.set_normal_cursor() + try: + self.button(QtGui.QWizard.NextButton).hide() + # Search duplicate songs. + max_songs = self.plugin.manager.get_object_count(Song) + if max_songs == 0 or max_songs == 1: + self.duplicate_search_progress_bar.setMaximum(1) + self.duplicate_search_progress_bar.setValue(1) + self.notify_no_duplicates() + return + # With x songs we have x*(x - 1) / 2 comparisons. + max_progress_count = max_songs * (max_songs - 1) // 2 + self.duplicate_search_progress_bar.setMaximum(max_progress_count) + songs = self.plugin.manager.get_all_objects(Song) + for outer_song_counter in range(max_songs - 1): + for inner_song_counter in range(outer_song_counter + 1, max_songs): + if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]): + duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter], + songs[inner_song_counter]) + if duplicate_added: + self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " + + songs[inner_song_counter].title) + self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1) + # The call to process_events() will keep the GUI responsive. + self.application.process_events() + if self.break_search: + return + self.review_total_count = len(self.duplicate_song_list) + if self.review_total_count == 0: + self.notify_no_duplicates() + else: + self.button(QtGui.QWizard.NextButton).show() + finally: + self.application.set_normal_cursor() elif page_id == self.review_page_id: self.process_current_duplicate_entry() @@ -238,6 +246,7 @@ class DuplicateSongRemovalForm(OpenLPWizard): Once the wizard is finished, refresh the song list, since we potentially removed songs from it. """ + self.break_search = True self.plugin.media_item.on_search_text_button_clicked() def setDefaults(self): @@ -337,3 +346,13 @@ class DuplicateSongRemovalForm(OpenLPWizard): return self._main_window main_window = property(_get_main_window) + + def _get_application(self): + """ + Adds the openlp to the class dynamically + """ + if not hasattr(self, u'_application'): + self._application = Registry().get(u'application') + return self._application + + application = property(_get_application) \ No newline at end of file From 4106ecd71e8dfa0b6dc838bc21d2bd3edf7c86ac Mon Sep 17 00:00:00 2001 From: Patrick Zimmermann Date: Sat, 15 Jun 2013 12:56:09 +0200 Subject: [PATCH 225/235] Remove superfluous comment. --- openlp/core/ui/slidecontroller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 24840e74d..5cec2659c 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -567,7 +567,6 @@ class SlideController(DisplayController): self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.screen = { u'size': self.preview_display.geometry()} - # Make sure that the frames have the correct size. self.onControllerSizeChanged(self.controller.width()) def onControllerSizeChanged(self, width): From d253ef5852215873e717ec48ab49821c4f711e2a Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 15 Jun 2013 21:51:11 +0100 Subject: [PATCH 226/235] Fix bug and add new test or bibles --- openlp/core/ui/slidecontroller.py | 18 +++++----- .../openlp_plugins/bibles/__init__.py | 1 + .../openlp_plugins/bibles/test_lib_http.py | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 tests/functional/openlp_plugins/bibles/__init__.py create mode 100644 tests/functional/openlp_plugins/bibles/test_lib_http.py diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 5cec2659c..2266026d6 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -591,7 +591,8 @@ class SlideController(DisplayController): """ request = self.sender().text() slide_no = self.slideList[request] - self.__updatePreviewSelection(slide_no) + width = self.main_window.controlSplitter.sizes()[self.split] + self.preview_widget.replace_service_item(self.service_item, width, slide_no) self.slideSelected() def receive_spin_delay(self): @@ -757,10 +758,8 @@ class SlideController(DisplayController): self.display.audio_player.play() self.setAudioItemsVisibility(True) row = 0 - text = [] width = self.main_window.controlSplitter.sizes()[self.split] for framenumber, frame in enumerate(self.service_item.get_frames()): - slideHeight = 0 if self.service_item.is_text(): if frame[u'verseTag']: # These tags are already translated. @@ -776,7 +775,6 @@ class SlideController(DisplayController): row += 1 self.slideList[unicode(row)] = row - 1 else: - slideHeight = width * (1 / self.ratio) row += 1 self.slideList[unicode(row)] = row - 1 # If current slide set background to image @@ -867,7 +865,7 @@ class SlideController(DisplayController): Settings().remove(self.main_window.general_settings_section + u'/screen blank') self.blankPlugin() self.updatePreview() - self.onToggleLoop() + self.on_toggle_loop() def onThemeDisplay(self, checked=None): """ @@ -886,7 +884,7 @@ class SlideController(DisplayController): Settings().remove(self.main_window.general_settings_section + u'/screen blank') self.blankPlugin() self.updatePreview() - self.onToggleLoop() + self.on_toggle_loop() def onHideDisplay(self, checked=None): """ @@ -905,7 +903,7 @@ class SlideController(DisplayController): Settings().remove(self.main_window.general_settings_section + u'/screen blank') self.hidePlugin(checked) self.updatePreview() - self.onToggleLoop() + self.on_toggle_loop() def blankPlugin(self): """ @@ -1073,7 +1071,7 @@ class SlideController(DisplayController): self.preview_widget.change_slide(row) self.slideSelected() - def onToggleLoop(self): + def on_toggle_loop(self): """ Toggles the loop state. """ @@ -1117,7 +1115,7 @@ class SlideController(DisplayController): else: self.play_slides_loop.setIcon(build_icon(u':/media/media_time.png')) self.play_slides_loop.setText(UiStrings().PlaySlidesInLoop) - self.onToggleLoop() + self.on_toggle_loop() def onPlaySlidesOnce(self, checked=None): """ @@ -1138,7 +1136,7 @@ class SlideController(DisplayController): else: self.play_slides_once.setIcon(build_icon(u':/media/media_time')) self.play_slides_once.setText(UiStrings().PlaySlidesToEnd) - self.onToggleLoop() + self.on_toggle_loop() def setAudioItemsVisibility(self, visible): """ diff --git a/tests/functional/openlp_plugins/bibles/__init__.py b/tests/functional/openlp_plugins/bibles/__init__.py new file mode 100644 index 000000000..f87606f07 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/__init__.py @@ -0,0 +1 @@ +__author__ = 'tim' diff --git a/tests/functional/openlp_plugins/bibles/test_lib_http.py b/tests/functional/openlp_plugins/bibles/test_lib_http.py new file mode 100644 index 000000000..b9982b42e --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_lib_http.py @@ -0,0 +1,33 @@ +""" + Package to test the openlp.plugin.bible.lib.https package. +""" + +from unittest import TestCase +from mock import MagicMock, patch + +from openlp.core.lib import Registry +from openlp.plugins.bibles.lib.http import BGExtract + + +class TestBibleHTTP(TestCase): + + def setUp(self): + """ + Set up the Registry + """ + Registry.create() + Registry().register(u'service_list', MagicMock()) + Registry().register(u'application', MagicMock()) + + def bible_gateway_extract_test(self): + """ + Test the Bible Gateway retrieval of book list for NIV + """ + # GIVEN: A new Bible Gateway extraction class + handler = BGExtract() + + # WHEN: The Books list is called + books = handler.get_books_from_http(u'NIV') + + # THEN: We should get back a valid service item + assert len(books) == 66, u'The bible should not have had its lenght changed' From e8d5ad773628b24c0ba9a76c35b7ba9b91de9b4e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 16 Jun 2013 08:54:16 +0100 Subject: [PATCH 227/235] Clean up slidecontroller --- openlp/core/ui/maindisplay.py | 2 +- openlp/core/ui/mainwindow.py | 2 +- openlp/core/ui/servicemanager.py | 12 +- openlp/core/ui/slidecontroller.py | 250 +++++++++--------- .../openlp_plugins/bibles/test_lib_http.py | 49 +++- 5 files changed, 177 insertions(+), 138 deletions(-) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 02ae469d6..7069cb9b7 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -288,7 +288,7 @@ class MainDisplay(Display): self.image(path) # Update the preview frame. if self.is_live: - self.live_controller.updatePreview() + self.live_controller.update_preview() return True def image(self, path): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 6b86bfe7a..4607c441f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -669,7 +669,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): Check and display message if screen blank on setup. """ settings = Settings() - self.live_controller.mainDisplaySetBackground() + self.live_controller.main_display_set_background() if settings.value(u'%s/screen blank' % self.general_settings_section): if settings.value(u'%s/blank warning' % self.general_settings_section): QtGui.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Main Display Blanked'), diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index c16e52c23..444edc814 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1267,7 +1267,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): newItem.merge(item[u'service_item']) item[u'service_item'] = newItem self.repaint_service_list(item_count + 1, 0) - self.live_controller.replaceServiceManagerItem(newItem) + self.live_controller.replace_service_manager_item(newItem) self.set_modified() def add_service_item(self, item, rebuild=False, expand=None, replace=False, repaint=True, selected=False): @@ -1289,7 +1289,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): item.merge(self.service_items[sitem][u'service_item']) self.service_items[sitem][u'service_item'] = item self.repaint_service_list(sitem, child) - self.live_controller.replaceServiceManagerItem(item) + self.live_controller.replace_service_manager_item(item) else: item.render() # nothing selected for dnd @@ -1312,7 +1312,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.repaint_service_list(self.drop_position, -1) # if rebuilding list make sure live is fixed. if rebuild: - self.live_controller.replaceServiceManagerItem(item) + self.live_controller.replace_service_manager_item(item) self.drop_position = 0 self.set_modified() @@ -1323,7 +1323,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.application.set_busy_cursor() item, child = self.find_service_item() if self.service_items[item][u'service_item'].is_valid: - self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], child) + self.preview_controller.add_service_manager_item(self.service_items[item][u'service_item'], child) else: critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'), translate('OpenLP.ServiceManager', @@ -1361,12 +1361,12 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): child = row self.application.set_busy_cursor() if self.service_items[item][u'service_item'].is_valid: - self.live_controller.addServiceManagerItem(self.service_items[item][u'service_item'], child) + self.live_controller.add_service_manager_item(self.service_items[item][u'service_item'], child) if Settings().value(self.main_window.general_settings_section + u'/auto preview'): item += 1 if self.service_items and item < len(self.service_items) and \ self.service_items[item][u'service_item'].is_capable(ItemCapabilities.CanPreview): - self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0) + self.preview_controller.add_service_manager_item(self.service_items[item][u'service_item'], 0) next_item = self.service_manager_list.topLevelItem(item) self.service_manager_list.setCurrentItem(next_item) self.live_controller.preview_widget.setFocus() diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 2266026d6..c9bb8b02c 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -194,15 +194,15 @@ class SlideController(DisplayController): self.toolbar.add_toolbar_widget(self.hide_menu) self.blank_screen = create_action(self, u'blankScreen', text=translate('OpenLP.SlideController', 'Blank Screen'), icon=u':/slides/slide_blank.png', - checked=False, can_shortcuts=True, category=self.category, triggers=self.onBlankDisplay) + checked=False, can_shortcuts=True, category=self.category, triggers=self.on_blank_display) self.theme_screen = create_action(self, u'themeScreen', text=translate('OpenLP.SlideController', 'Blank to Theme'), icon=u':/slides/slide_theme.png', checked=False, can_shortcuts=True, category=self.category, - triggers=self.onThemeDisplay) + triggers=self.on_theme_display) self.desktop_screen = create_action(self, u'desktopScreen', text=translate('OpenLP.SlideController', 'Show Desktop'), icon=u':/slides/slide_desktop.png', checked=False, can_shortcuts=True, category=self.category, - triggers=self.onHideDisplay) + triggers=self.on_hide_display) self.hide_menu.setDefaultAction(self.blank_screen) self.hide_menu.menu().addAction(self.blank_screen) self.hide_menu.menu().addAction(self.theme_screen) @@ -230,10 +230,10 @@ class SlideController(DisplayController): self.toolbar.add_toolbar_widget(self.play_slides_menu) self.play_slides_loop = create_action(self, u'playSlidesLoop', text=UiStrings().PlaySlidesInLoop, icon=u':/media/media_time.png', checked=False, can_shortcuts=True, - category=self.category, triggers=self.onPlaySlidesLoop) + category=self.category, triggers=self.on_play_slides_loop) self.play_slides_once = create_action(self, u'playSlidesOnce', text=UiStrings().PlaySlidesToEnd, icon=u':/media/media_time.png', checked=False, can_shortcuts=True, - category=self.category, triggers=self.onPlaySlidesOnce) + category=self.category, triggers=self.on_play_slides_once) if Settings().value(self.main_window.advanced_settings_section + u'/slide limits') == SlideLimits.Wrap: self.play_slides_menu.setDefaultAction(self.play_slides_loop) else: @@ -249,12 +249,12 @@ class SlideController(DisplayController): self.toolbar.add_toolbar_widget(self.delay_spin_box) else: self.toolbar.add_toolbar_action(u'goLive', icon=u':/general/general_live.png', - tooltip=translate('OpenLP.SlideController', 'Move to live.'), triggers=self.onGoLive) + tooltip=translate('OpenLP.SlideController', 'Move to live.'), triggers=self.on_go_live) self.toolbar.add_toolbar_action(u'addToService', icon=u':/general/general_add.png', - tooltip=translate('OpenLP.SlideController', 'Add to Service.'), triggers=self.onPreviewAddToService) + tooltip=translate('OpenLP.SlideController', 'Add to Service.'), triggers=self.on_preview_add_to_service) self.toolbar.addSeparator() self.toolbar.add_toolbar_action(u'editSong', icon=u':/general/general_edit.png', - tooltip=translate('OpenLP.SlideController', 'Edit and reload song preview.'), triggers=self.onEditSong) + tooltip=translate('OpenLP.SlideController', 'Edit and reload song preview.'), triggers=self.on_edit_song) self.controller_layout.addWidget(self.toolbar) # Build the Media Toolbar self.media_controller.register_controller(self) @@ -272,7 +272,7 @@ class SlideController(DisplayController): icon=u':/slides/media_playback_pause.png', text=translate('OpenLP.SlideController', 'Pause Audio'), tooltip=translate('OpenLP.SlideController', 'Pause audio.'), checked=False, visible=False, category=self.category, context=QtCore.Qt.WindowShortcut, - can_shortcuts=True, triggers=self.onAudioPauseClicked) + can_shortcuts=True, triggers=self.set_audio_pause_clicked) self.audio_menu = QtGui.QMenu(translate('OpenLP.SlideController', 'Background Audio'), self.toolbar) self.audio_pause_item.setMenu(self.audio_menu) self.audio_pause_item.setParent(self.toolbar) @@ -281,7 +281,7 @@ class SlideController(DisplayController): self.nextTrackItem = create_action(self, u'nextTrackItem', text=UiStrings().NextTrack, icon=u':/slides/media_playback_next.png', tooltip=translate('OpenLP.SlideController', 'Go to next audio track.'), - category=self.category, can_shortcuts=True, triggers=self.onNextTrackClicked) + category=self.category, can_shortcuts=True, triggers=self.on_next_track_clicked) self.audio_menu.addAction(self.nextTrackItem) self.trackMenu = self.audio_menu.addMenu(translate('OpenLP.SlideController', 'Tracks')) self.audio_time_label = QtGui.QLabel(u' 00:00 ', self.toolbar) @@ -348,10 +348,10 @@ class SlideController(DisplayController): can_shortcuts=True, context=QtCore.Qt.WidgetWithChildrenShortcut, category=self.category if s.get(u'configurable') else None, - triggers=self._slideShortcutActivated) for s in shortcuts]) - self.shortcutTimer.timeout.connect(self._slideShortcutActivated) + triggers=self._slide_shortcut_activated) for s in shortcuts]) + self.shortcutTimer.timeout.connect(self._slide_shortcut_activated) # Signals - self.preview_widget.clicked.connect(self.onSlideSelected) + self.preview_widget.clicked.connect(self.on_slide_selected) if self.is_live: # Need to use event as called across threads and UI is updated QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display) @@ -359,11 +359,11 @@ class SlideController(DisplayController): self.toolbar.set_widget_visible(self.loop_list, False) self.toolbar.set_widget_visible(self.wide_menu, False) else: - self.preview_widget.doubleClicked.connect(self.onGoLiveClick) + self.preview_widget.doubleClicked.connect(self.on_preview_add_to_service) self.toolbar.set_widget_visible([u'editSong'], False) if self.is_live: - self.setLiveHotkeys(self) - self.__addActionsToWidget(self.controller) + self.set_live_hotkeys(self) + self.__add_actions_to_widget(self.controller) else: self.controller.addActions([self.nextItem, self.previous_item]) Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop) @@ -378,7 +378,7 @@ class SlideController(DisplayController): QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_previous' % self.type_prefix), self.on_slide_selected_previous) - def _slideShortcutActivated(self): + def _slide_shortcut_activated(self): """ Called, when a shortcut has been activated to jump to a chorus, verse, etc. @@ -425,7 +425,7 @@ class SlideController(DisplayController): self.shortcutTimer.stop() self.current_shortcut = u'' self.preview_widget.change_slide(self.slideList[matches[0]]) - self.slideSelected() + self.slide_selected() elif sender_name != u'shortcutTimer': # Start the time as we did not have any match. self.shortcutTimer.start(350) @@ -435,28 +435,28 @@ class SlideController(DisplayController): # We had more than one match for example "V1" and "V10", but # "V1" was the slide we wanted to go. self.preview_widget.change_slide(self.slideList[self.current_shortcut]) - self.slideSelected() + self.slide_selected() # Reset the shortcut. self.current_shortcut = u'' - def setLiveHotkeys(self, parent=None): + def set_live_hotkeys(self, parent=None): """ Set the live hotkeys """ self.previousService = create_action(parent, u'previousService', text=translate('OpenLP.SlideController', 'Previous Service'), can_shortcuts=True, context=QtCore.Qt.WidgetWithChildrenShortcut, category=self.category, - triggers=self.servicePrevious) + triggers=self.service_previous) self.nextService = create_action(parent, 'nextService', text=translate('OpenLP.SlideController', 'Next Service'), can_shortcuts=True, context=QtCore.Qt.WidgetWithChildrenShortcut, category=self.category, - triggers=self.serviceNext) + triggers=self.service_next) self.escapeItem = create_action(parent, 'escapeItem', text=translate('OpenLP.SlideController', 'Escape Item'), can_shortcuts=True, context=QtCore.Qt.WidgetWithChildrenShortcut, category=self.category, - triggers=self.liveEscape) + triggers=self.live_escape) - def liveEscape(self): + def live_escape(self): """ If you press ESC on the live screen it should close the display temporarily. """ @@ -468,24 +468,24 @@ class SlideController(DisplayController): Toggle the display settings triggered from remote messages. """ if action == u'blank' or action == u'hide': - self.onBlankDisplay(True) + self.on_blank_display(True) elif action == u'theme': - self.onThemeDisplay(True) + self.on_theme_display(True) elif action == u'desktop': - self.onHideDisplay(True) + self.on_hide_display(True) elif action == u'show': - self.onBlankDisplay(False) - self.onThemeDisplay(False) - self.onHideDisplay(False) + self.on_blank_display(False) + self.on_theme_display(False) + self.on_hide_display(False) - def servicePrevious(self): + def service_previous(self): """ Live event to select the previous service item from the service manager. """ self.keypress_queue.append(ServiceItemAction.Previous) self._process_queue() - def serviceNext(self): + def service_next(self): """ Live event to select the next service item from the service manager. """ @@ -520,7 +520,7 @@ class SlideController(DisplayController): self.display = MainDisplay(self, self.is_live, self) self.display.setup() if self.is_live: - self.__addActionsToWidget(self.display) + self.__add_actions_to_widget(self.display) self.display.audio_player.connectSlot(QtCore.SIGNAL(u'tick(qint64)'), self.on_audio_time_remaining) # The SlidePreview's ratio. try: @@ -538,7 +538,7 @@ class SlideController(DisplayController): if self.service_item: self.refresh_service_item() - def __addActionsToWidget(self, widget): + def __add_actions_to_widget(self, widget): """ Add actions to the widget specified by `widget` """ @@ -567,9 +567,9 @@ class SlideController(DisplayController): self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.screen = { u'size': self.preview_display.geometry()} - self.onControllerSizeChanged(self.controller.width()) + self.on_controller_size_changed(self.controller.width()) - def onControllerSizeChanged(self, width): + def on_controller_size_changed(self, width): """ Change layout of display control buttons on controller size change """ @@ -585,7 +585,7 @@ class SlideController(DisplayController): self.toolbar.set_widget_visible(self.wide_menu, False) self.toolbar.set_widget_visible(self.narrow_menu) - def onSongBarHandler(self): + def on_song_bar_handler(self): """ Some song handler """ @@ -593,7 +593,7 @@ class SlideController(DisplayController): slide_no = self.slideList[request] width = self.main_window.controlSplitter.sizes()[self.split] self.preview_widget.replace_service_item(self.service_item, width, slide_no) - self.slideSelected() + self.slide_selected() def receive_spin_delay(self): """ @@ -681,49 +681,49 @@ class SlideController(DisplayController): """ log.debug(u'add_service_item live = %s' % self.is_live) item.render() - slideno = 0 + slide_no = 0 if self.song_edit: - slideno = self.selected_row + slide_no = self.selected_row self.song_edit = False - self._process_item(item, slideno) + self._process_item(item, slide_no) - def replaceServiceManagerItem(self, item): + def replace_service_manager_item(self, item): """ Replacement item following a remote edit """ if item == self.service_item: self._process_item(item, self.preview_widget.current_slide_number()) - def addServiceManagerItem(self, item, slideno): + def add_service_manager_item(self, item, slide_no): """ Method to install the service item into the controller and request the correct toolbar for the plugin. Called by ServiceManager """ - log.debug(u'addServiceManagerItem live = %s' % self.is_live) - # If no valid slide number is specified we take the first one, but we - # remember the initial value to see if we should reload the song or not - slidenum = slideno - if slideno == -1: + log.debug(u'add_service_manager_item live = %s' % self.is_live) + # If no valid slide number is specified we take the first one, but we remember the initial value to see if we + # should reload the song or not + slidenum = slide_no + if slide_no == -1: slidenum = 0 # If service item is the same as the current one, only change slide - if slideno >= 0 and item == self.service_item: + if slide_no >= 0 and item == self.service_item: self.preview_widget.change_slide(slidenum) - self.slideSelected() + self.slide_selected() else: self._process_item(item, slidenum) if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0: self.play_slides_loop.setChecked(item.auto_play_slides_loop) self.delay_spin_box.setValue(int(item.timed_slide_interval)) - self.onPlaySlidesLoop() + self.on_play_slides_loop() elif self.is_live and item.auto_play_slides_once and item.timed_slide_interval > 0: self.play_slides_once.setChecked(item.auto_play_slides_once) self.delay_spin_box.setValue(int(item.timed_slide_interval)) - self.onPlaySlidesOnce() + self.on_play_slides_once() def _process_item(self, service_item, slideno): """ - Loads a ServiceItem into the system from ServiceManager + Loads a ServiceItem into the system from ServiceManager Display the slide number passed """ log.debug(u'processManagerItem live = %s' % self.is_live) @@ -732,13 +732,13 @@ class SlideController(DisplayController): # take a copy not a link to the servicemanager copy. self.service_item = copy.copy(service_item) if old_item and self.is_live and old_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): - self._resetBlank() + self._reset_blank() Registry().execute(u'%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slideno]) self.slideList = {} if self.is_live: self.song_menu.menu().clear() self.display.audio_player.reset() - self.setAudioItemsVisibility(False) + self.set_audio_items_visibility(False) self.audio_pause_item.setChecked(False) # If the current item has background audio if self.service_item.is_capable(ItemCapabilities.HasBackgroundAudio): @@ -748,7 +748,7 @@ class SlideController(DisplayController): for counter in range(len(self.service_item.background_audio)): action = self.trackMenu.addAction(os.path.basename(self.service_item.background_audio[counter])) action.setData(counter) - action.triggered.connect(self.onTrackTriggered) + action.triggered.connect(self.on_track_triggered) self.display.audio_player.repeat = Settings().value( self.main_window.general_settings_section + u'/audio repeat list') if Settings().value(self.main_window.general_settings_section + u'/audio start paused'): @@ -756,7 +756,7 @@ class SlideController(DisplayController): self.display.audio_player.pause() else: self.display.audio_player.play() - self.setAudioItemsVisibility(True) + self.set_audio_items_visibility(True) row = 0 width = self.main_window.controlSplitter.sizes()[self.split] for framenumber, frame in enumerate(self.service_item.get_frames()): @@ -770,7 +770,7 @@ class SlideController(DisplayController): if verse_def not in self.slideList: self.slideList[verse_def] = framenumber if self.is_live: - self.song_menu.menu().addAction(verse_def, self.onSongBarHandler) + self.song_menu.menu().addAction(verse_def, self.on_song_bar_handler) else: row += 1 self.slideList[unicode(row)] = row - 1 @@ -789,8 +789,8 @@ class SlideController(DisplayController): if not self.service_item.is_image(): self.display.build_html(self.service_item) if service_item.is_media(): - self.onMediaStart(service_item) - self.slideSelected(True) + self.on_media_start(service_item) + self.slide_selected(True) if old_item: # Close the old item after the new one is opened # This avoids the service theme/desktop flashing on screen @@ -799,7 +799,7 @@ class SlideController(DisplayController): if old_item.is_command() and not service_item.is_command(): Registry().execute(u'%s_stop' % old_item.name.lower(), [old_item, self.is_live]) if old_item.is_media() and not service_item.is_media(): - self.onMediaClose() + self.on_media_close() Registry().execute(u'slidecontroller_%s_started' % self.type_prefix, [service_item]) # Screen event methods @@ -812,49 +812,49 @@ class SlideController(DisplayController): return if self.service_item.is_command(): Registry().execute(u'%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) - self.updatePreview() + self.update_preview() else: self.preview_widget.change_slide(index) - self.slideSelected() + self.slide_selected() - def mainDisplaySetBackground(self): + def main_display_set_background(self): """ Allow the main display to blank the main display at startup time """ - log.debug(u'mainDisplaySetBackground live = %s' % self.is_live) + log.debug(u'main_display_set_background live = %s' % self.is_live) display_type = Settings().value(self.main_window.general_settings_section + u'/screen blank') if self.screens.which_screen(self.window()) != self.screens.which_screen(self.display): # Order done to handle initial conversion if display_type == u'themed': - self.onThemeDisplay(True) + self.on_theme_display(True) elif display_type == u'hidden': - self.onHideDisplay(True) + self.on_hide_display(True) elif display_type == u'blanked': - self.onBlankDisplay(True) + self.on_blank_display(True) else: Registry().execute(u'live_display_show') else: - self.liveEscape() + self.live_escape() def on_slide_blank(self): """ Handle the slidecontroller blank event """ - self.onBlankDisplay(True) + self.on_blank_display(True) def on_slide_unblank(self): """ Handle the slidecontroller unblank event """ - self.onBlankDisplay(False) + self.on_blank_display(False) - def onBlankDisplay(self, checked=None): + def on_blank_display(self, checked=None): """ Handle the blank screen button actions """ if checked is None: checked = self.blank_screen.isChecked() - log.debug(u'onBlankDisplay %s' % checked) + log.debug(u'on_blank_display %s' % checked) self.hide_menu.setDefaultAction(self.blank_screen) self.blank_screen.setChecked(checked) self.theme_screen.setChecked(False) @@ -863,17 +863,17 @@ class SlideController(DisplayController): Settings().setValue(self.main_window.general_settings_section + u'/screen blank', u'blanked') else: Settings().remove(self.main_window.general_settings_section + u'/screen blank') - self.blankPlugin() - self.updatePreview() + self.blank_plugin() + self.update_preview() self.on_toggle_loop() - def onThemeDisplay(self, checked=None): + def on_theme_display(self, checked=None): """ Handle the Theme screen button """ if checked is None: checked = self.theme_screen.isChecked() - log.debug(u'onThemeDisplay %s' % checked) + log.debug(u'on_theme_display %s' % checked) self.hide_menu.setDefaultAction(self.theme_screen) self.blank_screen.setChecked(False) self.theme_screen.setChecked(checked) @@ -882,17 +882,17 @@ class SlideController(DisplayController): Settings().setValue(self.main_window.general_settings_section + u'/screen blank', u'themed') else: Settings().remove(self.main_window.general_settings_section + u'/screen blank') - self.blankPlugin() - self.updatePreview() + self.blank_plugin() + self.update_preview() self.on_toggle_loop() - def onHideDisplay(self, checked=None): + def on_hide_display(self, checked=None): """ Handle the Hide screen button """ if checked is None: checked = self.desktop_screen.isChecked() - log.debug(u'onHideDisplay %s' % checked) + log.debug(u'on_hide_display %s' % checked) self.hide_menu.setDefaultAction(self.desktop_screen) self.blank_screen.setChecked(False) self.theme_screen.setChecked(False) @@ -901,16 +901,16 @@ class SlideController(DisplayController): Settings().setValue(self.main_window.general_settings_section + u'/screen blank', u'hidden') else: Settings().remove(self.main_window.general_settings_section + u'/screen blank') - self.hidePlugin(checked) - self.updatePreview() + self.hide_plugin(checked) + self.update_preview() self.on_toggle_loop() - def blankPlugin(self): + def blank_plugin(self): """ Blank/Hide the display screen within a plugin if required. """ hide_mode = self.hide_mode() - log.debug(u'blankPlugin %s ', hide_mode) + log.debug(u'blank_plugin %s ', hide_mode) if self.service_item is not None: if hide_mode: if not self.service_item.is_command(): @@ -927,11 +927,11 @@ class SlideController(DisplayController): else: Registry().execute(u'live_display_show') - def hidePlugin(self, hide): + def hide_plugin(self, hide): """ Tell the plugin to hide the display screen. """ - log.debug(u'hidePlugin %s ', hide) + log.debug(u'hide_plugin %s ', hide) if self.service_item is not None: if hide: Registry().execute(u'live_display_hide', HideMode.Screen) @@ -946,13 +946,13 @@ class SlideController(DisplayController): else: Registry().execute(u'live_display_show') - def onSlideSelected(self): + def on_slide_selected(self): """ Slide selected in controller """ - self.slideSelected() + self.slide_selected() - def slideSelected(self, start=False): + def slide_selected(self, start=False): """ Generate the preview when you click on a slide. if this is the Live Controller also display on the screen @@ -975,7 +975,7 @@ class SlideController(DisplayController): self.display.image(to_display) # reset the store used to display first image self.service_item.bg_image_bytes = None - self.updatePreview() + self.update_preview() self.selected_row = row self.preview_widget.change_slide(row) Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) @@ -986,14 +986,14 @@ class SlideController(DisplayController): The slide has been changed. Update the slidecontroller accordingly """ self.preview_widget.change_slide(row) - self.updatePreview() + self.update_preview() Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row) - def updatePreview(self): + def update_preview(self): """ This updates the preview frame, for example after changing a slide or using *Blank to Theme*. """ - log.debug(u'updatePreview %s ' % self.screens.current[u'primary']) + log.debug(u'update_preview %s ' % self.screens.current[u'primary']) if not self.screens.current[u'primary'] and self.service_item and \ self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): # Grab now, but try again in a couple of seconds if slide change is slow @@ -1029,7 +1029,7 @@ class SlideController(DisplayController): return Registry().execute(u'%s_next' % self.service_item.name.lower(), [self.service_item, self.is_live]) if self.service_item.is_command() and self.is_live: - self.updatePreview() + self.update_preview() else: row = self.preview_widget.current_slide_number() + 1 if row == self.preview_widget.slide_count(): @@ -1037,7 +1037,7 @@ class SlideController(DisplayController): if self.slide_limits == SlideLimits.Wrap: row = 0 elif self.is_live and self.slide_limits == SlideLimits.Next: - self.serviceNext() + self.service_next() return else: row = self.preview_widget.slide_count() - 1 @@ -1046,7 +1046,7 @@ class SlideController(DisplayController): else: row = self.preview_widget.slide_count() - 1 self.preview_widget.change_slide(row) - self.slideSelected() + self.slide_selected() def on_slide_selected_previous(self): """ @@ -1056,7 +1056,7 @@ class SlideController(DisplayController): return Registry().execute(u'%s_previous' % self.service_item.name.lower(), [self.service_item, self.is_live]) if self.service_item.is_command() and self.is_live: - self.updatePreview() + self.update_preview() else: row = self.preview_widget.current_slide_number() - 1 if row == -1: @@ -1069,7 +1069,7 @@ class SlideController(DisplayController): else: row = 0 self.preview_widget.change_slide(row) - self.slideSelected() + self.slide_selected() def on_toggle_loop(self): """ @@ -1077,11 +1077,11 @@ class SlideController(DisplayController): """ hide_mode = self.hide_mode() if hide_mode is None and (self.play_slides_loop.isChecked() or self.play_slides_once.isChecked()): - self.onStartLoop() + self.on_start_loop() else: self.on_stop_loop() - def onStartLoop(self): + def on_start_loop(self): """ Start the timer loop running and store the timer id """ @@ -1096,7 +1096,7 @@ class SlideController(DisplayController): self.killTimer(self.timer_id) self.timer_id = 0 - def onPlaySlidesLoop(self, checked=None): + def on_play_slides_loop(self, checked=None): """ Start or stop 'Play Slides in Loop' """ @@ -1104,7 +1104,7 @@ class SlideController(DisplayController): checked = self.play_slides_loop.isChecked() else: self.play_slides_loop.setChecked(checked) - log.debug(u'onPlaySlidesLoop %s' % checked) + log.debug(u'on_play_slides_loop %s' % checked) if checked: self.play_slides_loop.setIcon(build_icon(u':/media/media_stop.png')) self.play_slides_loop.setText(UiStrings().StopPlaySlidesInLoop) @@ -1117,7 +1117,7 @@ class SlideController(DisplayController): self.play_slides_loop.setText(UiStrings().PlaySlidesInLoop) self.on_toggle_loop() - def onPlaySlidesOnce(self, checked=None): + def on_play_slides_once(self, checked=None): """ Start or stop 'Play Slides to End' """ @@ -1125,7 +1125,7 @@ class SlideController(DisplayController): checked = self.play_slides_once.isChecked() else: self.play_slides_once.setChecked(checked) - log.debug(u'onPlaySlidesOnce %s' % checked) + log.debug(u'on_play_slides_once %s' % checked) if checked: self.play_slides_once.setIcon(build_icon(u':/media/media_stop.png')) self.play_slides_once.setText(UiStrings().StopPlaySlidesToEnd) @@ -1138,13 +1138,13 @@ class SlideController(DisplayController): self.play_slides_once.setText(UiStrings().PlaySlidesToEnd) self.on_toggle_loop() - def setAudioItemsVisibility(self, visible): + def set_audio_items_visibility(self, visible): """ Set the visibility of the audio stuff """ self.toolbar.set_widget_visible(self.audio_list, visible) - def onAudioPauseClicked(self, checked): + def set_audio_pause_clicked(self, checked): """ Pause the audio player """ @@ -1162,7 +1162,7 @@ class SlideController(DisplayController): if event.timerId() == self.timer_id: self.on_slide_selected_next(self.play_slides_loop.isChecked()) - def onEditSong(self): + def on_edit_song(self): """ From the preview display requires the service Item to be editied """ @@ -1171,14 +1171,14 @@ class SlideController(DisplayController): if new_item: self.add_service_item(new_item) - def onPreviewAddToService(self): + def on_preview_add_to_service(self): """ From the preview display request the Item to be added to service """ if self.service_item: self.service_manager.add_service_item(self.service_item) - def onGoLiveClick(self): + def on_go_live_click(self): """ triggered by clicking the Preview slide items """ @@ -1188,10 +1188,10 @@ class SlideController(DisplayController): if self.service_item.is_command(): Registry().execute(u'%s_stop' % self.service_item.name.lower(), [self.service_item, self.is_live]) if self.service_item.is_media(): - self.onMediaClose() - self.onGoLive() + self.on_media_close() + self.on_go_live() - def onGoLive(self): + def on_go_live(self): """ If preview copy slide item to live controller from Preview Controller """ @@ -1200,41 +1200,41 @@ class SlideController(DisplayController): if self.service_item.from_service: self.service_manager.preview_live(self.service_item.unique_identifier, row) else: - self.live_controller.addServiceManagerItem(self.service_item, row) + self.live_controller.add_service_manager_item(self.service_item, row) - def onMediaStart(self, item): + def on_media_start(self, item): """ Respond to the arrival of a media service item """ - log.debug(u'SlideController onMediaStart') + log.debug(u'SlideController on_media_start') self.media_controller.video(self.controller_type, item, self.hide_mode()) if not self.is_live: self.preview_display.show() self.slide_preview.hide() - def onMediaClose(self): + def on_media_close(self): """ Respond to a request to close the Video """ - log.debug(u'SlideController onMediaClose') + log.debug(u'SlideController on_media_close') self.media_controller.media_reset(self) self.preview_display.hide() self.slide_preview.show() - def _resetBlank(self): + def _reset_blank(self): """ Used by command items which provide their own displays to reset the screen hide attributes """ hide_mode = self.hide_mode() if hide_mode == HideMode.Blank: - self.onBlankDisplay(True) + self.on_blank_display(True) elif hide_mode == HideMode.Theme: - self.onThemeDisplay(True) + self.on_theme_display(True) elif hide_mode == HideMode.Screen: - self.onHideDisplay(True) + self.on_hide_display(True) else: - self.hidePlugin(False) + self.hide_plugin(False) def hide_mode(self): """ @@ -1251,7 +1251,7 @@ class SlideController(DisplayController): else: return None - def onNextTrackClicked(self): + def on_next_track_clicked(self): """ Go to the next track when next is clicked """ @@ -1266,7 +1266,7 @@ class SlideController(DisplayController): seconds %= 60 self.audio_time_label.setText(u' %02d:%02d ' % (minutes, seconds)) - def onTrackTriggered(self): + def on_track_triggered(self): """ Start playing a track """ diff --git a/tests/functional/openlp_plugins/bibles/test_lib_http.py b/tests/functional/openlp_plugins/bibles/test_lib_http.py index b9982b42e..3d2e849de 100644 --- a/tests/functional/openlp_plugins/bibles/test_lib_http.py +++ b/tests/functional/openlp_plugins/bibles/test_lib_http.py @@ -3,10 +3,10 @@ """ from unittest import TestCase -from mock import MagicMock, patch +from mock import MagicMock from openlp.core.lib import Registry -from openlp.plugins.bibles.lib.http import BGExtract +from openlp.plugins.bibles.lib.http import BGExtract, CWExtract class TestBibleHTTP(TestCase): @@ -19,9 +19,9 @@ class TestBibleHTTP(TestCase): Registry().register(u'service_list', MagicMock()) Registry().register(u'application', MagicMock()) - def bible_gateway_extract_test(self): + def bible_gateway_extract_books_test(self): """ - Test the Bible Gateway retrieval of book list for NIV + Test the Bible Gateway retrieval of book list for NIV bible """ # GIVEN: A new Bible Gateway extraction class handler = BGExtract() @@ -30,4 +30,43 @@ class TestBibleHTTP(TestCase): books = handler.get_books_from_http(u'NIV') # THEN: We should get back a valid service item - assert len(books) == 66, u'The bible should not have had its lenght changed' + assert len(books) == 66, u'The bible should not have had any books added or removed' + + def bible_gateway_extract_verse_test(self): + """ + Test the Bible Gateway retrieval of verse list for NIV bible John 3 + """ + # GIVEN: A new Bible Gateway extraction class + handler = BGExtract() + + # WHEN: The Books list is called + results = handler.get_bible_chapter(u'NIV', u'John', 3) + + # THEN: We should get back a valid service item + assert len(results.verselist) == 36, u'The bible should not have had any books added or removed' + + def crosswalk_extract_books_test(self): + """ + Test Crosswalk retrieval of book list for NIV bible + """ + # GIVEN: A new Bible Gateway extraction class + handler = CWExtract() + + # WHEN: The Books list is called + books = handler.get_books_from_http(u'niv') + + # THEN: We should get back a valid service item + assert len(books) == 66, u'The bible should not have had any books added or removed' + + def crosswalk_extract_verse_test(self): + """ + Test the Crosswalk retrieval of verse list for NIV bible John 3 + """ + # GIVEN: A new Bible Gateway extraction class + handler = CWExtract() + + # WHEN: The Books list is called + results = handler.get_bible_chapter(u'niv', u'john', 3) + + # THEN: We should get back a valid service item + assert len(results.verselist) == 36, u'The bible should not have had any books added or removed' \ No newline at end of file From 7bf713a8303138dce7bc09321241b88fa00aa180 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 16 Jun 2013 09:28:38 +0100 Subject: [PATCH 228/235] minor updates --- tests/functional/openlp_plugins/bibles/test_lib_http.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_plugins/bibles/test_lib_http.py b/tests/functional/openlp_plugins/bibles/test_lib_http.py index 3d2e849de..a97c1a188 100644 --- a/tests/functional/openlp_plugins/bibles/test_lib_http.py +++ b/tests/functional/openlp_plugins/bibles/test_lib_http.py @@ -43,7 +43,7 @@ class TestBibleHTTP(TestCase): results = handler.get_bible_chapter(u'NIV', u'John', 3) # THEN: We should get back a valid service item - assert len(results.verselist) == 36, u'The bible should not have had any books added or removed' + assert len(results.verselist) == 36, u'The book of John should not have had any verses added or removed' def crosswalk_extract_books_test(self): """ @@ -69,4 +69,5 @@ class TestBibleHTTP(TestCase): results = handler.get_bible_chapter(u'niv', u'john', 3) # THEN: We should get back a valid service item - assert len(results.verselist) == 36, u'The bible should not have had any books added or removed' \ No newline at end of file + assert len(results.verselist) == 36, u'The book of John should not have had any verses added or removed' + From bb36a67ac2067fe2c5b490d4d167bdbb6abbbd6e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 16 Jun 2013 14:36:09 +0100 Subject: [PATCH 229/235] Move tests --- .../openlp_plugins/bibles/__init__.py | 0 .../openlp_plugins/bibles/test_lib_http.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{functional => interfaces}/openlp_plugins/bibles/__init__.py (100%) rename tests/{functional => interfaces}/openlp_plugins/bibles/test_lib_http.py (97%) diff --git a/tests/functional/openlp_plugins/bibles/__init__.py b/tests/interfaces/openlp_plugins/bibles/__init__.py similarity index 100% rename from tests/functional/openlp_plugins/bibles/__init__.py rename to tests/interfaces/openlp_plugins/bibles/__init__.py diff --git a/tests/functional/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py similarity index 97% rename from tests/functional/openlp_plugins/bibles/test_lib_http.py rename to tests/interfaces/openlp_plugins/bibles/test_lib_http.py index a97c1a188..c19f267ed 100644 --- a/tests/functional/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -60,7 +60,7 @@ class TestBibleHTTP(TestCase): def crosswalk_extract_verse_test(self): """ - Test the Crosswalk retrieval of verse list for NIV bible John 3 + Test Crosswalk retrieval of verse list for NIV bible John 3 """ # GIVEN: A new Bible Gateway extraction class handler = CWExtract() From bb7bbdf10c8e3caf8f9a2aa1b363ede278406976 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Sun, 16 Jun 2013 21:42:50 +0200 Subject: [PATCH 230/235] use isinstance instead of type --- openlp/plugins/images/lib/mediaitem.py | 2 +- scripts/check_dependencies.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index deed14594..3475570fc 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -473,7 +473,7 @@ class ImageMediaItem(MediaManagerItem): This boolean is set to True when the list in the interface should be reloaded after saving the new images """ for filename in images_list: - if type(filename) is not str and type(filename) is not unicode: + if not isinstance(filename, basestring): continue log.debug(u'Adding new image: %s', filename) imageFile = ImageFilenames() diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 16a2c10e7..6c1d528ac 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -98,9 +98,9 @@ OPTIONAL_MODULES = [ w = sys.stdout.write def check_vers(version, required, text): - if type(version) is not str: + if not isinstance(version, str): version = '.'.join(map(str, version)) - if type(required) is not str: + if not isinstance(required, str): required = '.'.join(map(str, required)) w(' %s >= %s ... ' % (text, required)) if LooseVersion(version) >= LooseVersion(required): From 62c5908d8555caa213ea15046aae007837db7be4 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 18 Jun 2013 11:09:54 +0200 Subject: [PATCH 231/235] python3 division in slidecontroller --- openlp/core/ui/pluginform.py | 2 +- openlp/core/ui/slidecontroller.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py index 954203d26..dd497bd68 100644 --- a/openlp/core/ui/pluginform.py +++ b/openlp/core/ui/pluginform.py @@ -31,7 +31,7 @@ The actual plugin view form """ import logging -from PyQt4 import QtCore, QtGui +from PyQt4 import QtGui from openlp.core.lib import PluginStatus, Registry, translate from plugindialog import Ui_PluginViewDialog diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index c9bb8b02c..427bb5cf1 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -29,6 +29,7 @@ """ The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller """ +from __future__ import division import os import logging import copy @@ -93,7 +94,7 @@ class SlideController(DisplayController): Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed) self.screens = ScreenList() try: - self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height()) + self.ratio = self.screens.current[u'size'].width() / self.screens.current[u'size'].height() except ZeroDivisionError: self.ratio = 1 self.loop_list = [ @@ -524,7 +525,7 @@ class SlideController(DisplayController): self.display.audio_player.connectSlot(QtCore.SIGNAL(u'tick(qint64)'), self.on_audio_time_remaining) # The SlidePreview's ratio. try: - self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height()) + self.ratio = self.screens.current[u'size'].width() / self.screens.current[u'size'].height() except ZeroDivisionError: self.ratio = 1 self.media_controller.setup_display(self.display, False) @@ -553,7 +554,7 @@ class SlideController(DisplayController): splitters is moved or when the screen size is changed. Note, that this method is (also) called frequently from the mainwindow *paintEvent*. """ - if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()): + if self.ratio < self.preview_frame.width() / self.preview_frame.height(): # We have to take the height as limit. max_height = self.preview_frame.height() - self.grid.margin() * 2 self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) @@ -723,7 +724,7 @@ class SlideController(DisplayController): def _process_item(self, service_item, slideno): """ - Loads a ServiceItem into the system from ServiceManager + Loads a ServiceItem into the system from ServiceManager Display the slide number passed """ log.debug(u'processManagerItem live = %s' % self.is_live) From 1e11f4d821430c78a37da112bd4b5b5b17afbe84 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 18 Jun 2013 11:12:49 +0200 Subject: [PATCH 232/235] true division --- openlp/core/ui/listpreviewwidget.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index ca04c9688..e15b83fbd 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -76,7 +76,7 @@ class ListPreviewWidget(QtGui.QTableWidget): else: # Sort out image heights. for framenumber in range(len(self.service_item.get_frames())): - height = self.viewport().width() / self.screen_ratio + height = self.viewport().width() // self.screen_ratio self.setRowHeight(framenumber, height) def screen_size_changed(self, screen_ratio): @@ -101,7 +101,7 @@ class ListPreviewWidget(QtGui.QTableWidget): for framenumber, frame in enumerate(self.service_item.get_frames()): self.setRowCount(self.slide_count() + 1) item = QtGui.QTableWidgetItem() - slideHeight = 0 + slide_height = 0 if self.service_item.is_text(): if frame[u'verseTag']: # These tags are already translated. @@ -125,12 +125,12 @@ class ListPreviewWidget(QtGui.QTableWidget): image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin) label.setPixmap(QtGui.QPixmap.fromImage(image)) self.setCellWidget(framenumber, 0, label) - slideHeight = width / self.screen_ratio + slide_height = width // self.screen_ratio row += 1 text.append(unicode(row)) self.setItem(framenumber, 0, item) - if slideHeight: - self.setRowHeight(framenumber, slideHeight) + if slide_height: + self.setRowHeight(framenumber, slide_height) self.setVerticalHeaderLabels(text) if self.service_item.is_text(): self.resizeRowsToContents() From 658b6f89dae34f024a865b7ab94e3f6928d1c0f0 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Tue, 18 Jun 2013 21:32:04 +0200 Subject: [PATCH 233/235] import --- openlp/core/ui/listpreviewwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index e15b83fbd..ae6d0bed8 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -30,7 +30,7 @@ The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller. It is based on a QTableWidget but represents its contents in list form. """ - +from __future__ import division from PyQt4 import QtCore, QtGui from openlp.core.lib import ImageSource, Registry, ServiceItem From aeecc041ec65b780eff4e0af9204038679484a0a Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Thu, 20 Jun 2013 20:04:21 +0200 Subject: [PATCH 234/235] removed spaces --- 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 0a594b5cf..bfd0b0740 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -346,9 +346,9 @@ def get_uno_instance(resolver): """ log.debug(u'get UNO Desktop Openoffice - resolve') if UNO_CONNECTION_TYPE == u'pipe': - return resolver.resolve(u'uno:pipe,name=openlp_pipe; urp;StarOffice.ComponentContext') + return resolver.resolve(u'uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') else: - return resolver.resolve(u'uno:socket,host=localhost,port=2002; urp;StarOffice.ComponentContext') + return resolver.resolve(u'uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') def format_time(text, local_time): From 0f0097c1a4510e9ae07cc4bdc4be1cdefdd33ea1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Thu, 20 Jun 2013 20:12:55 +0100 Subject: [PATCH 235/235] Fix presentations --- openlp/plugins/presentations/lib/mediaitem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index fcfc495ed..cef30a498 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -80,15 +80,15 @@ class PresentationMediaItem(MediaManagerItem): """ Build the list of file extensions to be used in the Open file dialog. """ - file_type = u'' + file_type_list = u'' for controller in self.controllers: if self.controllers[controller].enabled(): 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 + file_type_list += u'*.%s ' % file_type self.service_manager.supported_suffixes(file_type) - self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % file_type + self.on_new_file_masks = translate('PresentationPlugin.MediaItem', 'Presentations (%s)') % file_type_list def required_icons(self): """