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 |
Related bugs: |
|
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 : | # |
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() |
Linux tests passed!