Phill has proposed merging lp:~phill-ridout/openlp/fixes-IV into lp:openlp.
Commit message: Refactors and fixes Requested reviews: OpenLP Core (openlp-core) Related bugs: Bug #1600510 in OpenLP: "Deleting a songbook throws "song" attribute exception" https://bugs.launchpad.net/openlp/+bug/1600510 Bug #1673113 in OpenLP: ""Save & Preview" Button in Song Editor behaves in a strange way" https://bugs.launchpad.net/openlp/+bug/1673113 Bug #1799005 in OpenLP: "Searching for Bible Book of "Job" in Brazilian Portuguese (Jo) returns the wrong book. Returns Joshua." https://bugs.launchpad.net/openlp/+bug/1799005 Bug #1832942 in OpenLP: "version 2880 advanced setting alignment" https://bugs.launchpad.net/openlp/+bug/1832942 For more details, see: https://code.launchpad.net/~phill-ridout/openlp/fixes-IV/+merge/371023 Refactor `singleton` classes to use a `singleton` metaclass Fixes for a number of reported issues Tidy ups, and improvements as suggested by pycharm -- Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/fixes-IV into lp:openlp.
=== modified file 'openlp/core/common/__init__.py' --- openlp/core/common/__init__.py 2019-07-19 18:43:14 +0000 +++ openlp/core/common/__init__.py 2019-08-06 21:48:11 +0000 @@ -172,6 +172,21 @@ Next = 3 +class Singleton(type): + """ + Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + """ + Create a new instance if one does not already exist. + """ + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + def de_hump(name): """ Change any Camel Case string to python string @@ -385,7 +400,7 @@ global IMAGES_FILTER if not IMAGES_FILTER: log.debug('Generating images filter.') - formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats())))) + formats = list(map(bytes.decode, map(bytes, QtGui.QImageReader.supportedImageFormats()))) visible_formats = '(*.{text})'.format(text='; *.'.join(formats)) actual_formats = '(*.{text})'.format(text=' *.'.join(formats)) IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'), === modified file 'openlp/core/common/actions.py' --- openlp/core/common/actions.py 2019-04-13 13:00:22 +0000 +++ openlp/core/common/actions.py 2019-08-06 21:48:11 +0000 @@ -260,7 +260,7 @@ return # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O, # which is only done when we convert the strings in this way (QKeySequencet -> uncode). - shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts)))) + shortcuts = list(map(QtGui.QKeySequence.toString, map(QtGui.QKeySequence, shortcuts))) # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut # after removing the (initial) primary shortcut due to conflicts. if len(shortcuts) == 2: === modified file 'openlp/core/common/i18n.py' --- openlp/core/common/i18n.py 2019-07-03 13:23:23 +0000 +++ openlp/core/common/i18n.py 2019-08-06 21:48:11 +0000 @@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtWidgets -from openlp.core.common import is_macosx, is_win +from openlp.core.common import Singleton, is_macosx, is_win from openlp.core.common.applocation import AppLocation from openlp.core.common.settings import Settings @@ -327,22 +327,11 @@ return LanguageManager.__qm_list__ -class UiStrings(object): +class UiStrings(metaclass=Singleton): """ Provide standard strings for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = super().__new__(cls) - cls.__instance__.load() - return cls.__instance__ - - def load(self): + def __init__(self): """ These strings should need a good reason to be retranslated elsewhere. Should some/more/less of these have an & attached? @@ -436,6 +425,7 @@ self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.') self.RequiredShowInFooter = translate('OpenLP.Ui', 'Required, this will be displayed in footer.') self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds') + self.SaveAndClose = translate('OpenLP.ui', translate('SongsPlugin.EditSongForm', '&Save && Close')) self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview') self.Search = translate('OpenLP.Ui', 'Search') self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ') === modified file 'openlp/core/common/registry.py' --- openlp/core/common/registry.py 2019-05-05 18:41:17 +0000 +++ openlp/core/common/registry.py 2019-08-06 21:48:11 +0000 @@ -23,29 +23,19 @@ Provide Registry Services """ import logging -import sys -from openlp.core.common import de_hump, trace_error_handler +from openlp.core.common import Singleton, de_hump, trace_error_handler log = logging.getLogger(__name__) -class Registry(object): +class Registry(metaclass=Singleton): """ This is the Component Registry. It is a singleton object and is used to provide a look up service for common objects. """ log.info('Registry loaded') - __instance__ = None - - def __new__(cls): - """ - Re-implement the __new__ method to make sure we create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ @classmethod def create(cls): @@ -57,20 +47,9 @@ registry.service_list = {} registry.functions_list = {} registry.working_flags = {} - # Allow the tests to remove Registry entries but not the live system - registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0] registry.initialising = True return registry - @classmethod - def destroy(cls): - """ - Destroy the Registry. - """ - if cls.__instance__.running_under_test: - del cls.__instance__ - cls.__instance__ = None - def get(self, key): """ Extracts the registry value from the list based on the key passed in === modified file 'openlp/core/display/html/display.js' --- openlp/core/display/html/display.js 2019-06-21 20:53:42 +0000 +++ openlp/core/display/html/display.js 2019-08-06 21:48:11 +0000 @@ -281,8 +281,9 @@ * Checks if the present slide content fits within the slide */ doesContentFit: function () { - console.debug("scrollHeight: " + $(".slides")[0].scrollHeight + ", clientHeight: " + $(".slides")[0].clientHeight); - return $(".slides")[0].clientHeight >= $(".slides")[0].scrollHeight; + var currSlide = $(".slides")[0]; + console.debug("scrollHeight: " + currSlide.scrollHeight + ", clientHeight: " + currSlide.clientHeight); + return currSlide.clientHeight >= currSlide.scrollHeight; }, /** * Generate the OpenLP startup splashscreen @@ -333,7 +334,7 @@ /** * Set fullscreen image from base64 data * @param {string} bg_color - The background color - * @param {string} image - Path to the image + * @param {string} image_data - base64 encoded image data */ setFullscreenImageFromData: function(bg_color, image_data) { Display.clearSlides(); @@ -372,7 +373,6 @@ * @param {string} verse - The verse number, e.g. "v1" * @param {string} text - The HTML for the verse, e.g. "line1<br>line2" * @param {string} footer_text - The HTML for the footer" - * @param {bool} [reinit=true] - Re-initialize Reveal. Defaults to true. */ addTextSlide: function (verse, text, footer_text) { var html = _prepareText(text); @@ -476,25 +476,28 @@ * Play a video */ playVideo: function () { - if ($("#video").length == 1) { - $("#video")[0].play(); + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].play(); } }, /** * Pause a video */ pauseVideo: function () { - if ($("#video").length == 1) { - $("#video")[0].pause(); + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].pause(); } }, /** * Stop a video */ stopVideo: function () { - if ($("#video").length == 1) { - $("#video")[0].pause(); - $("#video")[0].currentTime = 0.0; + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].pause(); + videoElem[0].currentTime = 0.0; } }, /** @@ -502,8 +505,9 @@ * @param seconds The position in seconds to seek to */ seekVideo: function (seconds) { - if ($("#video").length == 1) { - $("#video")[0].currentTime = seconds; + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].currentTime = seconds; } }, /** @@ -511,8 +515,9 @@ * @param rate A Double of the rate. 1.0 => 100% speed, 0.75 => 75% speed, 1.25 => 125% speed, etc. */ setPlaybackRate: function (rate) { - if ($("#video").length == 1) { - $("#video")[0].playbackRate = rate; + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].playbackRate = rate; } }, /** @@ -520,24 +525,27 @@ * @param level The volume level from 0 to 100. */ setVideoVolume: function (level) { - if ($("#video").length == 1) { - $("#video")[0].volume = level / 100.0; + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].volume = level / 100.0; } }, /** * Mute the volume */ toggleVideoMute: function () { - if ($("#video").length == 1) { - $("#video")[0].muted = !$("#video")[0].muted; + var videoElem = $("#video"); + if (videoElem.length == 1) { + videoElem[0].muted = !videoElem[0].muted; } }, /** * Clear the background audio playlist */ clearPlaylist: function () { - if ($("#background-audio").length == 1) { - var audio = $("#background-audio")[0]; + var backgroundAudoElem = $("#background-audio"); + if (backgroundAudoElem.length == 1) { + var audio = backgroundAudoElem[0]; /* audio.playList */ } }, @@ -619,7 +627,6 @@ }, setTheme: function (theme) { this._theme = theme; - var slidesDiv = $(".slides") // Set the background var globalBackground = $("#global-background")[0]; var backgroundStyle = {}; === modified file 'openlp/core/display/render.py' --- openlp/core/display/render.py 2019-07-28 15:56:28 +0000 +++ openlp/core/display/render.py 2019-08-06 21:48:11 +0000 @@ -47,7 +47,7 @@ SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\' CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)' - r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?') + r'([\u0080-\uFFFF\w\s\.\,\!\?\;\:\|\"\'\-\_]*)(\Z)?') CHORD_TEMPLATE = '<span class="chordline">{chord}</span>' FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>' CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}' === modified file 'openlp/core/display/screens.py' --- openlp/core/display/screens.py 2019-08-04 14:06:00 +0000 +++ openlp/core/display/screens.py 2019-08-06 21:48:11 +0000 @@ -28,6 +28,7 @@ from PyQt5 import QtCore, QtWidgets +from openlp.core.common import Singleton from openlp.core.common.i18n import translate from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings @@ -147,24 +148,15 @@ screen_dict['custom_geometry']['height']) -class ScreenList(object): +class ScreenList(metaclass=Singleton): """ Wrapper to handle the parameters of the display screen. To get access to the screen list call ``ScreenList()``. """ log.info('Screen loaded') - __instance__ = None screens = [] - def __new__(cls): - """ - Re-implement __new__ to create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ - def __iter__(self): """ Convert this object into an iterable, so that we can iterate over it instead of the inner list === modified file 'openlp/core/lib/theme.py' --- openlp/core/lib/theme.py 2019-06-21 22:09:36 +0000 +++ openlp/core/lib/theme.py 2019-08-06 21:48:11 +0000 @@ -170,6 +170,7 @@ jsn = get_text_file_string(json_path) self.load_theme(jsn) self.background_filename = None + self.version = 2 def expand_json(self, var, prev=None): """ === modified file 'openlp/core/state.py' --- openlp/core/state.py 2019-04-13 13:00:22 +0000 +++ openlp/core/state.py 2019-08-06 21:48:11 +0000 @@ -28,6 +28,7 @@ """ import logging +from openlp.core.common import Singleton from openlp.core.common.registry import Registry from openlp.core.common.mixins import LogMixin from openlp.core.lib.plugin import PluginStatus @@ -52,17 +53,7 @@ self.text = None -class State(LogMixin): - - __instance__ = None - - def __new__(cls): - """ - Re-implement the __new__ method to make sure we create a true singleton. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ +class State(LogMixin, metaclass=Singleton): def load_settings(self): self.modules = {} === modified file 'openlp/core/ui/advancedtab.py' --- openlp/core/ui/advancedtab.py 2019-05-22 06:47:00 +0000 +++ openlp/core/ui/advancedtab.py 2019-08-06 21:48:11 +0000 @@ -81,7 +81,7 @@ self.ui_layout.addRow(self.media_plugin_check_box) self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.hide_mouse_check_box.setObjectName('hide_mouse_check_box') - self.ui_layout.addWidget(self.hide_mouse_check_box) + self.ui_layout.addRow(self.hide_mouse_check_box) self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.double_click_live_check_box.setObjectName('double_click_live_check_box') self.ui_layout.addRow(self.double_click_live_check_box) === modified file 'openlp/core/ui/icons.py' --- openlp/core/ui/icons.py 2019-07-03 13:23:23 +0000 +++ openlp/core/ui/icons.py 2019-08-06 21:48:11 +0000 @@ -27,6 +27,7 @@ import qtawesome as qta from PyQt5 import QtGui, QtWidgets +from openlp.core.common import Singleton from openlp.core.common.applocation import AppLocation from openlp.core.lib import build_icon @@ -34,22 +35,11 @@ log = logging.getLogger(__name__) -class UiIcons(object): +class UiIcons(metaclass=Singleton): """ Provide standard icons for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = super().__new__(cls) - cls.__instance__.load() - return cls.__instance__ - - def load(self): + def __init__(self): """ These are the font icons used in the code. """ @@ -165,6 +155,7 @@ 'volunteer': {'icon': 'fa.group'} } self.load_icons(icon_list) + self.main_icon = build_icon(':/icon/openlp-logo.svg') def load_icons(self, icon_list): """ @@ -184,7 +175,6 @@ setattr(self, key, qta.icon('fa.plus-circle', color='red')) except Exception: setattr(self, key, qta.icon('fa.plus-circle', color='red')) - self.main_icon = build_icon(':/icon/openlp-logo.svg') @staticmethod def _print_icons(): === modified file 'openlp/plugins/bibles/lib/__init__.py' --- openlp/plugins/bibles/lib/__init__.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/bibles/lib/__init__.py 2019-08-06 21:48:11 +0000 @@ -26,6 +26,7 @@ import logging import re +from openlp.core.common import Singleton from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings @@ -64,20 +65,10 @@ English = 2 -class BibleStrings(object): +class BibleStrings(metaclass=Singleton): """ Provide standard strings for objects to use. """ - __instance__ = None - - def __new__(cls): - """ - Override the default object creation method to return a single instance. - """ - if not cls.__instance__: - cls.__instance__ = object.__new__(cls) - return cls.__instance__ - def __init__(self): """ These strings should need a good reason to be retranslated elsewhere. @@ -336,11 +327,13 @@ log.debug('Matched reference {text}'.format(text=reference)) book = match.group('book') if not book_ref_id: - book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection) + book_ref_ids = bible.get_book_ref_id_by_localised_name(book, language_selection) elif not bible.get_book_by_book_ref_id(book_ref_id): return [] + else: + book_ref_ids = [book_ref_id] # We have not found the book so do not continue - if not book_ref_id: + if not book_ref_ids: return [] ranges = match.group('ranges') range_list = get_reference_match('range_separator').split(ranges) @@ -381,22 +374,23 @@ to_chapter = to_verse to_verse = None # Append references to the list - if has_range: - if not from_verse: - from_verse = 1 - if not to_verse: - to_verse = -1 - if to_chapter and to_chapter > from_chapter: - ref_list.append((book_ref_id, from_chapter, from_verse, -1)) - for i in range(from_chapter + 1, to_chapter): - ref_list.append((book_ref_id, i, 1, -1)) - ref_list.append((book_ref_id, to_chapter, 1, to_verse)) - elif to_verse >= from_verse or to_verse == -1: - ref_list.append((book_ref_id, from_chapter, from_verse, to_verse)) - elif from_verse: - ref_list.append((book_ref_id, from_chapter, from_verse, from_verse)) - else: - ref_list.append((book_ref_id, from_chapter, 1, -1)) + for book_ref_id in book_ref_ids: + if has_range: + if not from_verse: + from_verse = 1 + if not to_verse: + to_verse = -1 + if to_chapter and to_chapter > from_chapter: + ref_list.append((book_ref_id, from_chapter, from_verse, -1)) + for i in range(from_chapter + 1, to_chapter): + ref_list.append((book_ref_id, i, 1, -1)) + ref_list.append((book_ref_id, to_chapter, 1, to_verse)) + elif to_verse >= from_verse or to_verse == -1: + ref_list.append((book_ref_id, from_chapter, from_verse, to_verse)) + elif from_verse: + ref_list.append((book_ref_id, from_chapter, from_verse, from_verse)) + else: + ref_list.append((book_ref_id, from_chapter, 1, -1)) return ref_list else: log.debug('Invalid reference: {text}'.format(text=reference)) === modified file 'openlp/plugins/bibles/lib/db.py' --- openlp/plugins/bibles/lib/db.py 2019-05-22 06:47:00 +0000 +++ openlp/plugins/bibles/lib/db.py 2019-08-06 21:48:11 +0000 @@ -281,13 +281,14 @@ log.debug('BibleDB.get_book("{book}")'.format(book=book)) return self.get_object_filtered(Book, Book.name.like(book + '%')) - def get_books(self): + def get_books(self, book=None): """ A wrapper so both local and web bibles have a get_books() method that manager can call. Used in the media manager advanced search tab. """ - log.debug('BibleDB.get_books()') - return self.get_all_objects(Book, order_by_ref=Book.id) + log.debug('BibleDB.get_books("{book}")'.format(book=book)) + filter = Book.name.like(book + '%') if book else None + return self.get_all_objects(Book, filter_clause=filter, order_by_ref=Book.id) def get_book_by_book_ref_id(self, ref_id): """ @@ -300,39 +301,35 @@ def get_book_ref_id_by_localised_name(self, book, language_selection): """ - Return the id of a named book. + Return the ids of a matching named book. :param book: The name of the book, according to the selected language. :param language_selection: The language selection the user has chosen in the settings section of the Bible. + :rtype: list[int] """ log.debug('get_book_ref_id_by_localised_name("{book}", "{lang}")'.format(book=book, lang=language_selection)) from openlp.plugins.bibles.lib import LanguageSelection, BibleStrings book_names = BibleStrings().BookNames # escape reserved characters - book_escaped = book for character in RESERVED_CHARACTERS: - book_escaped = book_escaped.replace(character, '\\' + character) + book_escaped = book.replace(character, '\\' + character) regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE) if language_selection == LanguageSelection.Bible: - db_book = self.get_book(book) - if db_book: - return db_book.book_reference_id - elif language_selection == LanguageSelection.Application: - books = [key for key in list(book_names.keys()) if regex_book.match(str(book_names[key]))] - books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f] - for value in books: - if self.get_book_by_book_ref_id(value['id']): - return value['id'] - elif language_selection == LanguageSelection.English: - books = BiblesResourcesDB.get_books_like(book) - if books: - book_list = [value for value in books if regex_book.match(value['name'])] - if not book_list: - book_list = books - for value in book_list: - if self.get_book_by_book_ref_id(value['id']): - return value['id'] - return False + db_books = self.get_books(book) + return [db_book.book_reference_id for db_book in db_books] + else: + book_list = [] + if language_selection == LanguageSelection.Application: + books = [key for key in list(book_names.keys()) if regex_book.match(book_names[key])] + book_list = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f] + elif language_selection == LanguageSelection.English: + books = BiblesResourcesDB.get_books_like(book) + if books: + book_list = [value for value in books if regex_book.match(value['name'])] + if not book_list: + book_list = books + return [value['id'] for value in book_list if self.get_book_by_book_ref_id(value['id'])] + return [] def get_verses(self, reference_list, show_error=True): """ === modified file 'openlp/plugins/bibles/lib/manager.py' --- openlp/plugins/bibles/lib/manager.py 2019-07-03 13:23:23 +0000 +++ openlp/plugins/bibles/lib/manager.py 2019-08-06 21:48:11 +0000 @@ -240,8 +240,10 @@ book=book, chapter=chapter)) language_selection = self.get_language_selection(bible) - book_ref_id = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection) - return self.db_cache[bible].get_verse_count(book_ref_id, chapter) + book_ref_ids = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection) + if book_ref_ids: + return self.db_cache[bible].get_verse_count(book_ref_ids[0], chapter) + return 0 def get_verse_count_by_book_ref_id(self, bible, book_ref_id, chapter): """ === modified file 'openlp/plugins/custom/forms/editcustomdialog.py' --- openlp/plugins/custom/forms/editcustomdialog.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/custom/forms/editcustomdialog.py 2019-08-06 21:48:11 +0000 @@ -97,6 +97,7 @@ self.preview_button = QtWidgets.QPushButton() self.button_box = create_button_box(custom_edit_dialog, 'button_box', ['cancel', 'save'], [self.preview_button]) + self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save) self.dialog_layout.addWidget(self.button_box) self.retranslate_ui(custom_edit_dialog) @@ -112,3 +113,4 @@ self.theme_label.setText(translate('CustomPlugin.EditCustomForm', 'The&me:')) self.credit_label.setText(translate('CustomPlugin.EditCustomForm', '&Credits:')) self.preview_button.setText(UiStrings().SaveAndPreview) + self.save_button.setText(UiStrings().SaveAndClose) === modified file 'openlp/plugins/songs/forms/editsongdialog.py' --- openlp/plugins/songs/forms/editsongdialog.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/songs/forms/editsongdialog.py 2019-08-06 21:48:11 +0000 @@ -291,6 +291,7 @@ self.warning_label.setObjectName('warning_label') self.bottom_layout.addWidget(self.warning_label) self.button_box = create_button_box(edit_song_dialog, 'button_box', ['cancel', 'save']) + self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save) self.bottom_layout.addWidget(self.button_box) self.dialog_layout.addLayout(self.bottom_layout) self.retranslate_ui(edit_song_dialog) @@ -341,6 +342,7 @@ translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> Not all of the verses are in use.') self.no_verse_order_entered_warning = \ translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.') + self.save_button.setText(UiStrings().SaveAndPreview) def create_combo_box(parent, name, editable=True): === modified file 'openlp/plugins/songs/lib/db.py' --- openlp/plugins/songs/lib/db.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/songs/lib/db.py 2019-08-06 21:48:11 +0000 @@ -374,7 +374,9 @@ mapper(SongBookEntry, songs_songbooks_table, properties={ 'songbook': relation(Book) }) - mapper(Book, song_books_table) + mapper(Book, song_books_table, properties={ + 'songs': relation(Song, secondary=songs_songbooks_table) + }) mapper(MediaFile, media_files_table) mapper(Song, songs_table, properties={ # Use the authors_songs relation when you need access to the 'author_type' attribute === modified file 'openlp/plugins/songs/lib/importers/cclifile.py' --- openlp/plugins/songs/lib/importers/cclifile.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/songs/lib/importers/cclifile.py 2019-08-06 21:48:11 +0000 @@ -146,7 +146,9 @@ """ log.debug('USR file text: {text}'.format(text=text_list)) song_author = '' + song_fields = '' song_topics = '' + song_words = '' for line in text_list: if line.startswith('[S '): ccli, line = line.split(']', 1) === modified file 'openlp/plugins/songs/lib/importers/dreambeam.py' --- openlp/plugins/songs/lib/importers/dreambeam.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/songs/lib/importers/dreambeam.py 2019-08-06 21:48:11 +0000 @@ -87,6 +87,7 @@ if self.stop_import_flag: return self.set_defaults() + author_copyright = '' parser = etree.XMLParser(remove_blank_text=True) try: with file_path.open('r') as xml_file: @@ -142,7 +143,7 @@ author_copyright = song_xml.Text2.Text.text if author_copyright: author_copyright = str(author_copyright) - if author_copyright.find(str(SongStrings.CopyrightSymbol)) >= 0: + if author_copyright.find(SongStrings.CopyrightSymbol) >= 0: self.add_copyright(author_copyright) else: self.parse_author(author_copyright) === modified file 'openlp/plugins/songs/lib/importers/easyslides.py' --- openlp/plugins/songs/lib/importers/easyslides.py 2019-04-13 13:00:22 +0000 +++ openlp/plugins/songs/lib/importers/easyslides.py 2019-08-06 21:48:11 +0000 @@ -137,9 +137,11 @@ except UnicodeDecodeError: log.exception('Unicode decode error while decoding Contents') self._success = False + return except AttributeError: log.exception('no Contents') self._success = False + return lines = lyrics.split('\n') # we go over all lines first, to determine information, # which tells us how to parse verses later === modified file 'openlp/plugins/songs/lib/importers/easyworship.py' --- openlp/plugins/songs/lib/importers/easyworship.py 2019-07-03 13:23:23 +0000 +++ openlp/plugins/songs/lib/importers/easyworship.py 2019-08-06 21:48:11 +0000 @@ -268,13 +268,13 @@ self.db_set_record_struct(field_descriptions) # Pick out the field description indexes we will need try: - success = True fi_title = self.db_find_field(b'Title') fi_author = self.db_find_field(b'Author') fi_copy = self.db_find_field(b'Copyright') fi_admin = self.db_find_field(b'Administrator') fi_words = self.db_find_field(b'Words') fi_ccli = self.db_find_field(b'Song Number') + success = True except IndexError: # This is the wrong table success = False === modified file 'openlp/plugins/songs/lib/importers/songbeamer.py' --- openlp/plugins/songs/lib/importers/songbeamer.py 2019-05-22 06:47:00 +0000 +++ openlp/plugins/songs/lib/importers/songbeamer.py 2019-08-06 21:48:11 +0000 @@ -128,7 +128,7 @@ # The encoding should only be ANSI (cp1252), UTF-8, Unicode, Big-Endian-Unicode. # So if it doesn't start with 'u' we default to cp1252. See: # https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2 - if not self.input_file_encoding.lower().startswith('u'): + if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'): self.input_file_encoding = 'cp1252' with file_path.open(encoding=self.input_file_encoding) as song_file: song_data = song_file.readlines() === modified file 'tests/functional/openlp_core/common/test_common.py' --- tests/functional/openlp_core/common/test_common.py 2019-05-22 06:47:00 +0000 +++ tests/functional/openlp_core/common/test_common.py 2019-08-06 21:48:11 +0000 @@ -26,7 +26,7 @@ from unittest import TestCase from unittest.mock import MagicMock, call, patch -from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \ +from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \ normalize_str, path_to_module, trace_error_handler @@ -163,6 +163,48 @@ mocked_logger.error.assert_called_with( 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test') + def test_singleton_metaclass_multiple_init(self): + """ + Test that a class using the Singleton Metaclass is only initialised once despite being called several times and + that the same instance is returned each time.. + """ + # GIVEN: The Singleton Metaclass and a test class using it + class SingletonClass(metaclass=Singleton): + def __init__(self): + pass + + with patch.object(SingletonClass, '__init__', return_value=None) as patched_init: + + # WHEN: Initialising the class multiple times + inst_1 = SingletonClass() + inst_2 = SingletonClass() + + # THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values + # should be the same instance. + assert inst_1 is inst_2 + assert patched_init.call_count == 1 + + def test_singleton_metaclass_multiple_classes(self): + """ + Test that multiple classes using the Singleton Metaclass return the different an appropriate instances. + """ + # GIVEN: Two different classes using the Singleton Metaclass + class SingletonClass1(metaclass=Singleton): + def __init__(self): + pass + + class SingletonClass2(metaclass=Singleton): + def __init__(self): + pass + + # WHEN: Initialising both classes + s_c1 = SingletonClass1() + s_c2 = SingletonClass2() + + # THEN: The instances should be an instance of the appropriate class + assert isinstance(s_c1, SingletonClass1) + assert isinstance(s_c2, SingletonClass2) + def test_is_win(self): """ Test the is_win() function === modified file 'tests/functional/openlp_core/lib/test_theme.py' --- tests/functional/openlp_core/lib/test_theme.py 2019-07-18 19:14:58 +0000 +++ tests/functional/openlp_core/lib/test_theme.py 2019-08-06 21:48:11 +0000 @@ -182,4 +182,4 @@ assert 0 == theme.display_vertical_align, 'display_vertical_align should be 0' assert theme.font_footer_bold is False, 'font_footer_bold should be False' assert 'Arial' == theme.font_main_name, 'font_main_name should be "Arial"' - assert 47 == len(theme.__dict__), 'The theme should have 47 attributes' + assert 48 == len(theme.__dict__), 'The theme should have 48 attributes' === modified file 'tests/functional/openlp_core/ui/test_icons.py' --- tests/functional/openlp_core/ui/test_icons.py 2019-04-13 13:00:22 +0000 +++ tests/functional/openlp_core/ui/test_icons.py 2019-08-06 21:48:11 +0000 @@ -33,7 +33,7 @@ class TestIcons(TestCase, TestMixin): - @patch('openlp.core.ui.icons.UiIcons.load') + @patch('openlp.core.ui.icons.UiIcons.__init__', return_value=None) def test_simple_icon(self, _): # GIVEN: an basic set of icons icons = UiIcons()
_______________________________________________ Mailing list: https://launchpad.net/~openlp-core Post to : openlp-core@lists.launchpad.net Unsubscribe : https://launchpad.net/~openlp-core More help : https://help.launchpad.net/ListHelp