Merge lp:~phill-ridout/openlp/fixes-IV into lp:openlp

Proposed by Phill
Status: Merged
Merged at revision: 2889
Proposed branch: lp:~phill-ridout/openlp/fixes-IV
Merge into: lp:openlp
Diff against target: 852 lines (+172/-161)
25 files modified
openlp/core/common/__init__.py (+16/-1)
openlp/core/common/actions.py (+1/-1)
openlp/core/common/i18n.py (+4/-14)
openlp/core/common/registry.py (+2/-23)
openlp/core/display/html/display.js (+29/-22)
openlp/core/display/render.py (+1/-1)
openlp/core/display/screens.py (+2/-10)
openlp/core/lib/theme.py (+1/-0)
openlp/core/state.py (+2/-11)
openlp/core/ui/advancedtab.py (+1/-1)
openlp/core/ui/icons.py (+4/-14)
openlp/plugins/bibles/lib/__init__.py (+23/-29)
openlp/plugins/bibles/lib/db.py (+22/-25)
openlp/plugins/bibles/lib/manager.py (+4/-2)
openlp/plugins/custom/forms/editcustomdialog.py (+2/-0)
openlp/plugins/songs/forms/editsongdialog.py (+2/-0)
openlp/plugins/songs/lib/db.py (+3/-1)
openlp/plugins/songs/lib/importers/cclifile.py (+2/-0)
openlp/plugins/songs/lib/importers/dreambeam.py (+2/-1)
openlp/plugins/songs/lib/importers/easyslides.py (+2/-0)
openlp/plugins/songs/lib/importers/easyworship.py (+1/-1)
openlp/plugins/songs/lib/importers/songbeamer.py (+1/-1)
tests/functional/openlp_core/common/test_common.py (+43/-1)
tests/functional/openlp_core/lib/test_theme.py (+1/-1)
tests/functional/openlp_core/ui/test_icons.py (+1/-1)
To merge this branch: bzr merge lp:~phill-ridout/openlp/fixes-IV
Reviewer Review Type Date Requested Status
Tim Bentley Approve
Review via email: mp+371023@code.launchpad.net

Commit message

Refactors and fixes

Description of the change

Refactor `singleton` classes to use a `singleton` metaclass
Fixes for a number of reported issues
Tidy ups, and improvements as suggested by pycharm

To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
Tim Bentley (trb143) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/common/__init__.py'
2--- openlp/core/common/__init__.py 2019-07-19 18:43:14 +0000
3+++ openlp/core/common/__init__.py 2019-08-06 21:48:11 +0000
4@@ -172,6 +172,21 @@
5 Next = 3
6
7
8+class Singleton(type):
9+ """
10+ Provide a `Singleton` metaclass https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python
11+ """
12+ _instances = {}
13+
14+ def __call__(cls, *args, **kwargs):
15+ """
16+ Create a new instance if one does not already exist.
17+ """
18+ if cls not in cls._instances:
19+ cls._instances[cls] = super().__call__(*args, **kwargs)
20+ return cls._instances[cls]
21+
22+
23 def de_hump(name):
24 """
25 Change any Camel Case string to python string
26@@ -385,7 +400,7 @@
27 global IMAGES_FILTER
28 if not IMAGES_FILTER:
29 log.debug('Generating images filter.')
30- formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
31+ formats = list(map(bytes.decode, map(bytes, QtGui.QImageReader.supportedImageFormats())))
32 visible_formats = '(*.{text})'.format(text='; *.'.join(formats))
33 actual_formats = '(*.{text})'.format(text=' *.'.join(formats))
34 IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'),
35
36=== modified file 'openlp/core/common/actions.py'
37--- openlp/core/common/actions.py 2019-04-13 13:00:22 +0000
38+++ openlp/core/common/actions.py 2019-08-06 21:48:11 +0000
39@@ -260,7 +260,7 @@
40 return
41 # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
42 # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
43- shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
44+ shortcuts = list(map(QtGui.QKeySequence.toString, map(QtGui.QKeySequence, shortcuts)))
45 # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
46 # after removing the (initial) primary shortcut due to conflicts.
47 if len(shortcuts) == 2:
48
49=== modified file 'openlp/core/common/i18n.py'
50--- openlp/core/common/i18n.py 2019-07-03 13:23:23 +0000
51+++ openlp/core/common/i18n.py 2019-08-06 21:48:11 +0000
52@@ -29,7 +29,7 @@
53
54 from PyQt5 import QtCore, QtWidgets
55
56-from openlp.core.common import is_macosx, is_win
57+from openlp.core.common import Singleton, is_macosx, is_win
58 from openlp.core.common.applocation import AppLocation
59 from openlp.core.common.settings import Settings
60
61@@ -327,22 +327,11 @@
62 return LanguageManager.__qm_list__
63
64
65-class UiStrings(object):
66+class UiStrings(metaclass=Singleton):
67 """
68 Provide standard strings for objects to use.
69 """
70- __instance__ = None
71-
72- def __new__(cls):
73- """
74- Override the default object creation method to return a single instance.
75- """
76- if not cls.__instance__:
77- cls.__instance__ = super().__new__(cls)
78- cls.__instance__.load()
79- return cls.__instance__
80-
81- def load(self):
82+ def __init__(self):
83 """
84 These strings should need a good reason to be retranslated elsewhere.
85 Should some/more/less of these have an & attached?
86@@ -436,6 +425,7 @@
87 self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.')
88 self.RequiredShowInFooter = translate('OpenLP.Ui', 'Required, this will be displayed in footer.')
89 self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
90+ self.SaveAndClose = translate('OpenLP.ui', translate('SongsPlugin.EditSongForm', '&Save && Close'))
91 self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
92 self.Search = translate('OpenLP.Ui', 'Search')
93 self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')
94
95=== modified file 'openlp/core/common/registry.py'
96--- openlp/core/common/registry.py 2019-05-05 18:41:17 +0000
97+++ openlp/core/common/registry.py 2019-08-06 21:48:11 +0000
98@@ -23,29 +23,19 @@
99 Provide Registry Services
100 """
101 import logging
102-import sys
103
104-from openlp.core.common import de_hump, trace_error_handler
105+from openlp.core.common import Singleton, de_hump, trace_error_handler
106
107
108 log = logging.getLogger(__name__)
109
110
111-class Registry(object):
112+class Registry(metaclass=Singleton):
113 """
114 This is the Component Registry. It is a singleton object and is used to provide a look up service for common
115 objects.
116 """
117 log.info('Registry loaded')
118- __instance__ = None
119-
120- def __new__(cls):
121- """
122- Re-implement the __new__ method to make sure we create a true singleton.
123- """
124- if not cls.__instance__:
125- cls.__instance__ = object.__new__(cls)
126- return cls.__instance__
127
128 @classmethod
129 def create(cls):
130@@ -57,20 +47,9 @@
131 registry.service_list = {}
132 registry.functions_list = {}
133 registry.working_flags = {}
134- # Allow the tests to remove Registry entries but not the live system
135- registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0]
136 registry.initialising = True
137 return registry
138
139- @classmethod
140- def destroy(cls):
141- """
142- Destroy the Registry.
143- """
144- if cls.__instance__.running_under_test:
145- del cls.__instance__
146- cls.__instance__ = None
147-
148 def get(self, key):
149 """
150 Extracts the registry value from the list based on the key passed in
151
152=== modified file 'openlp/core/display/html/display.js'
153--- openlp/core/display/html/display.js 2019-06-21 20:53:42 +0000
154+++ openlp/core/display/html/display.js 2019-08-06 21:48:11 +0000
155@@ -281,8 +281,9 @@
156 * Checks if the present slide content fits within the slide
157 */
158 doesContentFit: function () {
159- console.debug("scrollHeight: " + $(".slides")[0].scrollHeight + ", clientHeight: " + $(".slides")[0].clientHeight);
160- return $(".slides")[0].clientHeight >= $(".slides")[0].scrollHeight;
161+ var currSlide = $(".slides")[0];
162+ console.debug("scrollHeight: " + currSlide.scrollHeight + ", clientHeight: " + currSlide.clientHeight);
163+ return currSlide.clientHeight >= currSlide.scrollHeight;
164 },
165 /**
166 * Generate the OpenLP startup splashscreen
167@@ -333,7 +334,7 @@
168 /**
169 * Set fullscreen image from base64 data
170 * @param {string} bg_color - The background color
171- * @param {string} image - Path to the image
172+ * @param {string} image_data - base64 encoded image data
173 */
174 setFullscreenImageFromData: function(bg_color, image_data) {
175 Display.clearSlides();
176@@ -372,7 +373,6 @@
177 * @param {string} verse - The verse number, e.g. "v1"
178 * @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
179 * @param {string} footer_text - The HTML for the footer"
180- * @param {bool} [reinit=true] - Re-initialize Reveal. Defaults to true.
181 */
182 addTextSlide: function (verse, text, footer_text) {
183 var html = _prepareText(text);
184@@ -476,25 +476,28 @@
185 * Play a video
186 */
187 playVideo: function () {
188- if ($("#video").length == 1) {
189- $("#video")[0].play();
190+ var videoElem = $("#video");
191+ if (videoElem.length == 1) {
192+ videoElem[0].play();
193 }
194 },
195 /**
196 * Pause a video
197 */
198 pauseVideo: function () {
199- if ($("#video").length == 1) {
200- $("#video")[0].pause();
201+ var videoElem = $("#video");
202+ if (videoElem.length == 1) {
203+ videoElem[0].pause();
204 }
205 },
206 /**
207 * Stop a video
208 */
209 stopVideo: function () {
210- if ($("#video").length == 1) {
211- $("#video")[0].pause();
212- $("#video")[0].currentTime = 0.0;
213+ var videoElem = $("#video");
214+ if (videoElem.length == 1) {
215+ videoElem[0].pause();
216+ videoElem[0].currentTime = 0.0;
217 }
218 },
219 /**
220@@ -502,8 +505,9 @@
221 * @param seconds The position in seconds to seek to
222 */
223 seekVideo: function (seconds) {
224- if ($("#video").length == 1) {
225- $("#video")[0].currentTime = seconds;
226+ var videoElem = $("#video");
227+ if (videoElem.length == 1) {
228+ videoElem[0].currentTime = seconds;
229 }
230 },
231 /**
232@@ -511,8 +515,9 @@
233 * @param rate A Double of the rate. 1.0 => 100% speed, 0.75 => 75% speed, 1.25 => 125% speed, etc.
234 */
235 setPlaybackRate: function (rate) {
236- if ($("#video").length == 1) {
237- $("#video")[0].playbackRate = rate;
238+ var videoElem = $("#video");
239+ if (videoElem.length == 1) {
240+ videoElem[0].playbackRate = rate;
241 }
242 },
243 /**
244@@ -520,24 +525,27 @@
245 * @param level The volume level from 0 to 100.
246 */
247 setVideoVolume: function (level) {
248- if ($("#video").length == 1) {
249- $("#video")[0].volume = level / 100.0;
250+ var videoElem = $("#video");
251+ if (videoElem.length == 1) {
252+ videoElem[0].volume = level / 100.0;
253 }
254 },
255 /**
256 * Mute the volume
257 */
258 toggleVideoMute: function () {
259- if ($("#video").length == 1) {
260- $("#video")[0].muted = !$("#video")[0].muted;
261+ var videoElem = $("#video");
262+ if (videoElem.length == 1) {
263+ videoElem[0].muted = !videoElem[0].muted;
264 }
265 },
266 /**
267 * Clear the background audio playlist
268 */
269 clearPlaylist: function () {
270- if ($("#background-audio").length == 1) {
271- var audio = $("#background-audio")[0];
272+ var backgroundAudoElem = $("#background-audio");
273+ if (backgroundAudoElem.length == 1) {
274+ var audio = backgroundAudoElem[0];
275 /* audio.playList */
276 }
277 },
278@@ -619,7 +627,6 @@
279 },
280 setTheme: function (theme) {
281 this._theme = theme;
282- var slidesDiv = $(".slides")
283 // Set the background
284 var globalBackground = $("#global-background")[0];
285 var backgroundStyle = {};
286
287=== modified file 'openlp/core/display/render.py'
288--- openlp/core/display/render.py 2019-07-28 15:56:28 +0000
289+++ openlp/core/display/render.py 2019-08-06 21:48:11 +0000
290@@ -47,7 +47,7 @@
291
292 SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\'
293 CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)'
294- r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?')
295+ r'([\u0080-\uFFFF\w\s\.\,\!\?\;\:\|\"\'\-\_]*)(\Z)?')
296 CHORD_TEMPLATE = '<span class="chordline">{chord}</span>'
297 FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>'
298 CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'
299
300=== modified file 'openlp/core/display/screens.py'
301--- openlp/core/display/screens.py 2019-08-04 14:06:00 +0000
302+++ openlp/core/display/screens.py 2019-08-06 21:48:11 +0000
303@@ -28,6 +28,7 @@
304
305 from PyQt5 import QtCore, QtWidgets
306
307+from openlp.core.common import Singleton
308 from openlp.core.common.i18n import translate
309 from openlp.core.common.registry import Registry
310 from openlp.core.common.settings import Settings
311@@ -147,24 +148,15 @@
312 screen_dict['custom_geometry']['height'])
313
314
315-class ScreenList(object):
316+class ScreenList(metaclass=Singleton):
317 """
318 Wrapper to handle the parameters of the display screen.
319
320 To get access to the screen list call ``ScreenList()``.
321 """
322 log.info('Screen loaded')
323- __instance__ = None
324 screens = []
325
326- def __new__(cls):
327- """
328- Re-implement __new__ to create a true singleton.
329- """
330- if not cls.__instance__:
331- cls.__instance__ = object.__new__(cls)
332- return cls.__instance__
333-
334 def __iter__(self):
335 """
336 Convert this object into an iterable, so that we can iterate over it instead of the inner list
337
338=== modified file 'openlp/core/lib/theme.py'
339--- openlp/core/lib/theme.py 2019-06-21 22:09:36 +0000
340+++ openlp/core/lib/theme.py 2019-08-06 21:48:11 +0000
341@@ -170,6 +170,7 @@
342 jsn = get_text_file_string(json_path)
343 self.load_theme(jsn)
344 self.background_filename = None
345+ self.version = 2
346
347 def expand_json(self, var, prev=None):
348 """
349
350=== modified file 'openlp/core/state.py'
351--- openlp/core/state.py 2019-04-13 13:00:22 +0000
352+++ openlp/core/state.py 2019-08-06 21:48:11 +0000
353@@ -28,6 +28,7 @@
354 """
355 import logging
356
357+from openlp.core.common import Singleton
358 from openlp.core.common.registry import Registry
359 from openlp.core.common.mixins import LogMixin
360 from openlp.core.lib.plugin import PluginStatus
361@@ -52,17 +53,7 @@
362 self.text = None
363
364
365-class State(LogMixin):
366-
367- __instance__ = None
368-
369- def __new__(cls):
370- """
371- Re-implement the __new__ method to make sure we create a true singleton.
372- """
373- if not cls.__instance__:
374- cls.__instance__ = object.__new__(cls)
375- return cls.__instance__
376+class State(LogMixin, metaclass=Singleton):
377
378 def load_settings(self):
379 self.modules = {}
380
381=== modified file 'openlp/core/ui/advancedtab.py'
382--- openlp/core/ui/advancedtab.py 2019-05-22 06:47:00 +0000
383+++ openlp/core/ui/advancedtab.py 2019-08-06 21:48:11 +0000
384@@ -81,7 +81,7 @@
385 self.ui_layout.addRow(self.media_plugin_check_box)
386 self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box)
387 self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
388- self.ui_layout.addWidget(self.hide_mouse_check_box)
389+ self.ui_layout.addRow(self.hide_mouse_check_box)
390 self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box)
391 self.double_click_live_check_box.setObjectName('double_click_live_check_box')
392 self.ui_layout.addRow(self.double_click_live_check_box)
393
394=== modified file 'openlp/core/ui/icons.py'
395--- openlp/core/ui/icons.py 2019-07-03 13:23:23 +0000
396+++ openlp/core/ui/icons.py 2019-08-06 21:48:11 +0000
397@@ -27,6 +27,7 @@
398 import qtawesome as qta
399 from PyQt5 import QtGui, QtWidgets
400
401+from openlp.core.common import Singleton
402 from openlp.core.common.applocation import AppLocation
403 from openlp.core.lib import build_icon
404
405@@ -34,22 +35,11 @@
406 log = logging.getLogger(__name__)
407
408
409-class UiIcons(object):
410+class UiIcons(metaclass=Singleton):
411 """
412 Provide standard icons for objects to use.
413 """
414- __instance__ = None
415-
416- def __new__(cls):
417- """
418- Override the default object creation method to return a single instance.
419- """
420- if not cls.__instance__:
421- cls.__instance__ = super().__new__(cls)
422- cls.__instance__.load()
423- return cls.__instance__
424-
425- def load(self):
426+ def __init__(self):
427 """
428 These are the font icons used in the code.
429 """
430@@ -165,6 +155,7 @@
431 'volunteer': {'icon': 'fa.group'}
432 }
433 self.load_icons(icon_list)
434+ self.main_icon = build_icon(':/icon/openlp-logo.svg')
435
436 def load_icons(self, icon_list):
437 """
438@@ -184,7 +175,6 @@
439 setattr(self, key, qta.icon('fa.plus-circle', color='red'))
440 except Exception:
441 setattr(self, key, qta.icon('fa.plus-circle', color='red'))
442- self.main_icon = build_icon(':/icon/openlp-logo.svg')
443
444 @staticmethod
445 def _print_icons():
446
447=== modified file 'openlp/plugins/bibles/lib/__init__.py'
448--- openlp/plugins/bibles/lib/__init__.py 2019-04-13 13:00:22 +0000
449+++ openlp/plugins/bibles/lib/__init__.py 2019-08-06 21:48:11 +0000
450@@ -26,6 +26,7 @@
451 import logging
452 import re
453
454+from openlp.core.common import Singleton
455 from openlp.core.common.i18n import translate
456 from openlp.core.common.settings import Settings
457
458@@ -64,20 +65,10 @@
459 English = 2
460
461
462-class BibleStrings(object):
463+class BibleStrings(metaclass=Singleton):
464 """
465 Provide standard strings for objects to use.
466 """
467- __instance__ = None
468-
469- def __new__(cls):
470- """
471- Override the default object creation method to return a single instance.
472- """
473- if not cls.__instance__:
474- cls.__instance__ = object.__new__(cls)
475- return cls.__instance__
476-
477 def __init__(self):
478 """
479 These strings should need a good reason to be retranslated elsewhere.
480@@ -336,11 +327,13 @@
481 log.debug('Matched reference {text}'.format(text=reference))
482 book = match.group('book')
483 if not book_ref_id:
484- book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
485+ book_ref_ids = bible.get_book_ref_id_by_localised_name(book, language_selection)
486 elif not bible.get_book_by_book_ref_id(book_ref_id):
487 return []
488+ else:
489+ book_ref_ids = [book_ref_id]
490 # We have not found the book so do not continue
491- if not book_ref_id:
492+ if not book_ref_ids:
493 return []
494 ranges = match.group('ranges')
495 range_list = get_reference_match('range_separator').split(ranges)
496@@ -381,22 +374,23 @@
497 to_chapter = to_verse
498 to_verse = None
499 # Append references to the list
500- if has_range:
501- if not from_verse:
502- from_verse = 1
503- if not to_verse:
504- to_verse = -1
505- if to_chapter and to_chapter > from_chapter:
506- ref_list.append((book_ref_id, from_chapter, from_verse, -1))
507- for i in range(from_chapter + 1, to_chapter):
508- ref_list.append((book_ref_id, i, 1, -1))
509- ref_list.append((book_ref_id, to_chapter, 1, to_verse))
510- elif to_verse >= from_verse or to_verse == -1:
511- ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
512- elif from_verse:
513- ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
514- else:
515- ref_list.append((book_ref_id, from_chapter, 1, -1))
516+ for book_ref_id in book_ref_ids:
517+ if has_range:
518+ if not from_verse:
519+ from_verse = 1
520+ if not to_verse:
521+ to_verse = -1
522+ if to_chapter and to_chapter > from_chapter:
523+ ref_list.append((book_ref_id, from_chapter, from_verse, -1))
524+ for i in range(from_chapter + 1, to_chapter):
525+ ref_list.append((book_ref_id, i, 1, -1))
526+ ref_list.append((book_ref_id, to_chapter, 1, to_verse))
527+ elif to_verse >= from_verse or to_verse == -1:
528+ ref_list.append((book_ref_id, from_chapter, from_verse, to_verse))
529+ elif from_verse:
530+ ref_list.append((book_ref_id, from_chapter, from_verse, from_verse))
531+ else:
532+ ref_list.append((book_ref_id, from_chapter, 1, -1))
533 return ref_list
534 else:
535 log.debug('Invalid reference: {text}'.format(text=reference))
536
537=== modified file 'openlp/plugins/bibles/lib/db.py'
538--- openlp/plugins/bibles/lib/db.py 2019-05-22 06:47:00 +0000
539+++ openlp/plugins/bibles/lib/db.py 2019-08-06 21:48:11 +0000
540@@ -281,13 +281,14 @@
541 log.debug('BibleDB.get_book("{book}")'.format(book=book))
542 return self.get_object_filtered(Book, Book.name.like(book + '%'))
543
544- def get_books(self):
545+ def get_books(self, book=None):
546 """
547 A wrapper so both local and web bibles have a get_books() method that
548 manager can call. Used in the media manager advanced search tab.
549 """
550- log.debug('BibleDB.get_books()')
551- return self.get_all_objects(Book, order_by_ref=Book.id)
552+ log.debug('BibleDB.get_books("{book}")'.format(book=book))
553+ filter = Book.name.like(book + '%') if book else None
554+ return self.get_all_objects(Book, filter_clause=filter, order_by_ref=Book.id)
555
556 def get_book_by_book_ref_id(self, ref_id):
557 """
558@@ -300,39 +301,35 @@
559
560 def get_book_ref_id_by_localised_name(self, book, language_selection):
561 """
562- Return the id of a named book.
563+ Return the ids of a matching named book.
564
565 :param book: The name of the book, according to the selected language.
566 :param language_selection: The language selection the user has chosen in the settings section of the Bible.
567+ :rtype: list[int]
568 """
569 log.debug('get_book_ref_id_by_localised_name("{book}", "{lang}")'.format(book=book, lang=language_selection))
570 from openlp.plugins.bibles.lib import LanguageSelection, BibleStrings
571 book_names = BibleStrings().BookNames
572 # escape reserved characters
573- book_escaped = book
574 for character in RESERVED_CHARACTERS:
575- book_escaped = book_escaped.replace(character, '\\' + character)
576+ book_escaped = book.replace(character, '\\' + character)
577 regex_book = re.compile('\\s*{book}\\s*'.format(book='\\s*'.join(book_escaped.split())), re.IGNORECASE)
578 if language_selection == LanguageSelection.Bible:
579- db_book = self.get_book(book)
580- if db_book:
581- return db_book.book_reference_id
582- elif language_selection == LanguageSelection.Application:
583- books = [key for key in list(book_names.keys()) if regex_book.match(str(book_names[key]))]
584- books = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
585- for value in books:
586- if self.get_book_by_book_ref_id(value['id']):
587- return value['id']
588- elif language_selection == LanguageSelection.English:
589- books = BiblesResourcesDB.get_books_like(book)
590- if books:
591- book_list = [value for value in books if regex_book.match(value['name'])]
592- if not book_list:
593- book_list = books
594- for value in book_list:
595- if self.get_book_by_book_ref_id(value['id']):
596- return value['id']
597- return False
598+ db_books = self.get_books(book)
599+ return [db_book.book_reference_id for db_book in db_books]
600+ else:
601+ book_list = []
602+ if language_selection == LanguageSelection.Application:
603+ books = [key for key in list(book_names.keys()) if regex_book.match(book_names[key])]
604+ book_list = [_f for _f in map(BiblesResourcesDB.get_book, books) if _f]
605+ elif language_selection == LanguageSelection.English:
606+ books = BiblesResourcesDB.get_books_like(book)
607+ if books:
608+ book_list = [value for value in books if regex_book.match(value['name'])]
609+ if not book_list:
610+ book_list = books
611+ return [value['id'] for value in book_list if self.get_book_by_book_ref_id(value['id'])]
612+ return []
613
614 def get_verses(self, reference_list, show_error=True):
615 """
616
617=== modified file 'openlp/plugins/bibles/lib/manager.py'
618--- openlp/plugins/bibles/lib/manager.py 2019-07-03 13:23:23 +0000
619+++ openlp/plugins/bibles/lib/manager.py 2019-08-06 21:48:11 +0000
620@@ -240,8 +240,10 @@
621 book=book,
622 chapter=chapter))
623 language_selection = self.get_language_selection(bible)
624- book_ref_id = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
625- return self.db_cache[bible].get_verse_count(book_ref_id, chapter)
626+ book_ref_ids = self.db_cache[bible].get_book_ref_id_by_localised_name(book, language_selection)
627+ if book_ref_ids:
628+ return self.db_cache[bible].get_verse_count(book_ref_ids[0], chapter)
629+ return 0
630
631 def get_verse_count_by_book_ref_id(self, bible, book_ref_id, chapter):
632 """
633
634=== modified file 'openlp/plugins/custom/forms/editcustomdialog.py'
635--- openlp/plugins/custom/forms/editcustomdialog.py 2019-04-13 13:00:22 +0000
636+++ openlp/plugins/custom/forms/editcustomdialog.py 2019-08-06 21:48:11 +0000
637@@ -97,6 +97,7 @@
638 self.preview_button = QtWidgets.QPushButton()
639 self.button_box = create_button_box(custom_edit_dialog, 'button_box', ['cancel', 'save'],
640 [self.preview_button])
641+ self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
642 self.dialog_layout.addWidget(self.button_box)
643 self.retranslate_ui(custom_edit_dialog)
644
645@@ -112,3 +113,4 @@
646 self.theme_label.setText(translate('CustomPlugin.EditCustomForm', 'The&me:'))
647 self.credit_label.setText(translate('CustomPlugin.EditCustomForm', '&Credits:'))
648 self.preview_button.setText(UiStrings().SaveAndPreview)
649+ self.save_button.setText(UiStrings().SaveAndClose)
650
651=== modified file 'openlp/plugins/songs/forms/editsongdialog.py'
652--- openlp/plugins/songs/forms/editsongdialog.py 2019-04-13 13:00:22 +0000
653+++ openlp/plugins/songs/forms/editsongdialog.py 2019-08-06 21:48:11 +0000
654@@ -291,6 +291,7 @@
655 self.warning_label.setObjectName('warning_label')
656 self.bottom_layout.addWidget(self.warning_label)
657 self.button_box = create_button_box(edit_song_dialog, 'button_box', ['cancel', 'save'])
658+ self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
659 self.bottom_layout.addWidget(self.button_box)
660 self.dialog_layout.addLayout(self.bottom_layout)
661 self.retranslate_ui(edit_song_dialog)
662@@ -341,6 +342,7 @@
663 translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> Not all of the verses are in use.')
664 self.no_verse_order_entered_warning = \
665 translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
666+ self.save_button.setText(UiStrings().SaveAndPreview)
667
668
669 def create_combo_box(parent, name, editable=True):
670
671=== modified file 'openlp/plugins/songs/lib/db.py'
672--- openlp/plugins/songs/lib/db.py 2019-04-13 13:00:22 +0000
673+++ openlp/plugins/songs/lib/db.py 2019-08-06 21:48:11 +0000
674@@ -374,7 +374,9 @@
675 mapper(SongBookEntry, songs_songbooks_table, properties={
676 'songbook': relation(Book)
677 })
678- mapper(Book, song_books_table)
679+ mapper(Book, song_books_table, properties={
680+ 'songs': relation(Song, secondary=songs_songbooks_table)
681+ })
682 mapper(MediaFile, media_files_table)
683 mapper(Song, songs_table, properties={
684 # Use the authors_songs relation when you need access to the 'author_type' attribute
685
686=== modified file 'openlp/plugins/songs/lib/importers/cclifile.py'
687--- openlp/plugins/songs/lib/importers/cclifile.py 2019-04-13 13:00:22 +0000
688+++ openlp/plugins/songs/lib/importers/cclifile.py 2019-08-06 21:48:11 +0000
689@@ -146,7 +146,9 @@
690 """
691 log.debug('USR file text: {text}'.format(text=text_list))
692 song_author = ''
693+ song_fields = ''
694 song_topics = ''
695+ song_words = ''
696 for line in text_list:
697 if line.startswith('[S '):
698 ccli, line = line.split(']', 1)
699
700=== modified file 'openlp/plugins/songs/lib/importers/dreambeam.py'
701--- openlp/plugins/songs/lib/importers/dreambeam.py 2019-04-13 13:00:22 +0000
702+++ openlp/plugins/songs/lib/importers/dreambeam.py 2019-08-06 21:48:11 +0000
703@@ -87,6 +87,7 @@
704 if self.stop_import_flag:
705 return
706 self.set_defaults()
707+ author_copyright = ''
708 parser = etree.XMLParser(remove_blank_text=True)
709 try:
710 with file_path.open('r') as xml_file:
711@@ -142,7 +143,7 @@
712 author_copyright = song_xml.Text2.Text.text
713 if author_copyright:
714 author_copyright = str(author_copyright)
715- if author_copyright.find(str(SongStrings.CopyrightSymbol)) >= 0:
716+ if author_copyright.find(SongStrings.CopyrightSymbol) >= 0:
717 self.add_copyright(author_copyright)
718 else:
719 self.parse_author(author_copyright)
720
721=== modified file 'openlp/plugins/songs/lib/importers/easyslides.py'
722--- openlp/plugins/songs/lib/importers/easyslides.py 2019-04-13 13:00:22 +0000
723+++ openlp/plugins/songs/lib/importers/easyslides.py 2019-08-06 21:48:11 +0000
724@@ -137,9 +137,11 @@
725 except UnicodeDecodeError:
726 log.exception('Unicode decode error while decoding Contents')
727 self._success = False
728+ return
729 except AttributeError:
730 log.exception('no Contents')
731 self._success = False
732+ return
733 lines = lyrics.split('\n')
734 # we go over all lines first, to determine information,
735 # which tells us how to parse verses later
736
737=== modified file 'openlp/plugins/songs/lib/importers/easyworship.py'
738--- openlp/plugins/songs/lib/importers/easyworship.py 2019-07-03 13:23:23 +0000
739+++ openlp/plugins/songs/lib/importers/easyworship.py 2019-08-06 21:48:11 +0000
740@@ -268,13 +268,13 @@
741 self.db_set_record_struct(field_descriptions)
742 # Pick out the field description indexes we will need
743 try:
744- success = True
745 fi_title = self.db_find_field(b'Title')
746 fi_author = self.db_find_field(b'Author')
747 fi_copy = self.db_find_field(b'Copyright')
748 fi_admin = self.db_find_field(b'Administrator')
749 fi_words = self.db_find_field(b'Words')
750 fi_ccli = self.db_find_field(b'Song Number')
751+ success = True
752 except IndexError:
753 # This is the wrong table
754 success = False
755
756=== modified file 'openlp/plugins/songs/lib/importers/songbeamer.py'
757--- openlp/plugins/songs/lib/importers/songbeamer.py 2019-05-22 06:47:00 +0000
758+++ openlp/plugins/songs/lib/importers/songbeamer.py 2019-08-06 21:48:11 +0000
759@@ -128,7 +128,7 @@
760 # The encoding should only be ANSI (cp1252), UTF-8, Unicode, Big-Endian-Unicode.
761 # So if it doesn't start with 'u' we default to cp1252. See:
762 # https://forum.songbeamer.com/viewtopic.php?p=419&sid=ca4814924e37c11e4438b7272a98b6f2
763- if not self.input_file_encoding.lower().startswith('u'):
764+ if self.input_file_encoding and not self.input_file_encoding.lower().startswith('u'):
765 self.input_file_encoding = 'cp1252'
766 with file_path.open(encoding=self.input_file_encoding) as song_file:
767 song_data = song_file.readlines()
768
769=== modified file 'tests/functional/openlp_core/common/test_common.py'
770--- tests/functional/openlp_core/common/test_common.py 2019-05-22 06:47:00 +0000
771+++ tests/functional/openlp_core/common/test_common.py 2019-08-06 21:48:11 +0000
772@@ -26,7 +26,7 @@
773 from unittest import TestCase
774 from unittest.mock import MagicMock, call, patch
775
776-from openlp.core.common import clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
777+from openlp.core.common import Singleton, clean_button_text, de_hump, extension_loader, is_linux, is_macosx, is_win, \
778 normalize_str, path_to_module, trace_error_handler
779
780
781@@ -163,6 +163,48 @@
782 mocked_logger.error.assert_called_with(
783 'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test')
784
785+ def test_singleton_metaclass_multiple_init(self):
786+ """
787+ Test that a class using the Singleton Metaclass is only initialised once despite being called several times and
788+ that the same instance is returned each time..
789+ """
790+ # GIVEN: The Singleton Metaclass and a test class using it
791+ class SingletonClass(metaclass=Singleton):
792+ def __init__(self):
793+ pass
794+
795+ with patch.object(SingletonClass, '__init__', return_value=None) as patched_init:
796+
797+ # WHEN: Initialising the class multiple times
798+ inst_1 = SingletonClass()
799+ inst_2 = SingletonClass()
800+
801+ # THEN: The __init__ method of the SingletonClass should have only been called once, and both returned values
802+ # should be the same instance.
803+ assert inst_1 is inst_2
804+ assert patched_init.call_count == 1
805+
806+ def test_singleton_metaclass_multiple_classes(self):
807+ """
808+ Test that multiple classes using the Singleton Metaclass return the different an appropriate instances.
809+ """
810+ # GIVEN: Two different classes using the Singleton Metaclass
811+ class SingletonClass1(metaclass=Singleton):
812+ def __init__(self):
813+ pass
814+
815+ class SingletonClass2(metaclass=Singleton):
816+ def __init__(self):
817+ pass
818+
819+ # WHEN: Initialising both classes
820+ s_c1 = SingletonClass1()
821+ s_c2 = SingletonClass2()
822+
823+ # THEN: The instances should be an instance of the appropriate class
824+ assert isinstance(s_c1, SingletonClass1)
825+ assert isinstance(s_c2, SingletonClass2)
826+
827 def test_is_win(self):
828 """
829 Test the is_win() function
830
831=== modified file 'tests/functional/openlp_core/lib/test_theme.py'
832--- tests/functional/openlp_core/lib/test_theme.py 2019-07-18 19:14:58 +0000
833+++ tests/functional/openlp_core/lib/test_theme.py 2019-08-06 21:48:11 +0000
834@@ -182,4 +182,4 @@
835 assert 0 == theme.display_vertical_align, 'display_vertical_align should be 0'
836 assert theme.font_footer_bold is False, 'font_footer_bold should be False'
837 assert 'Arial' == theme.font_main_name, 'font_main_name should be "Arial"'
838- assert 47 == len(theme.__dict__), 'The theme should have 47 attributes'
839+ assert 48 == len(theme.__dict__), 'The theme should have 48 attributes'
840
841=== modified file 'tests/functional/openlp_core/ui/test_icons.py'
842--- tests/functional/openlp_core/ui/test_icons.py 2019-04-13 13:00:22 +0000
843+++ tests/functional/openlp_core/ui/test_icons.py 2019-08-06 21:48:11 +0000
844@@ -33,7 +33,7 @@
845
846 class TestIcons(TestCase, TestMixin):
847
848- @patch('openlp.core.ui.icons.UiIcons.load')
849+ @patch('openlp.core.ui.icons.UiIcons.__init__', return_value=None)
850 def test_simple_icon(self, _):
851 # GIVEN: an basic set of icons
852 icons = UiIcons()