Merge lp:~phill-ridout/openlp/ftw-json-theme-list into lp:openlp
- ftw-json-theme-list
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~phill-ridout/openlp/ftw-json-theme-list |
Merge into: | lp:openlp |
Diff against target: |
961 lines (+395/-269) 6 files modified
openlp/core/common/httputils.py (+47/-1) openlp/core/ui/firsttimeform.py (+118/-186) openlp/core/ui/firsttimewizard.py (+50/-15) openlp/core/ui/icons.py (+2/-0) tests/functional/openlp_core/ui/test_firsttimeform.py (+90/-67) tests/interfaces/openlp_core/ui/test_firsttimeform.py (+88/-0) |
To merge this branch: | bzr merge lp:~phill-ridout/openlp/ftw-json-theme-list |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenLP Core | Pending | ||
Review via email: mp+363275@code.launchpad.net |
This proposal has been superseded by a proposal from 2019-02-16.
Commit message
move ftw to new json config format. spruce up theme list page
Description of the change
lp:~phill-ridout/openlp/ftw-json-theme-list (revision 2852)
https:/
https:/
https:/
https:/
https:/
https:/
https:/
https:/
Raoul Snyman (raoul-snyman) wrote : | # |
Raoul Snyman (raoul-snyman) wrote : | # |
Linting passed!
Raoul Snyman (raoul-snyman) wrote : | # |
macOS tests passed!
Unmerged revisions
Preview Diff
1 | === modified file 'openlp/core/common/httputils.py' |
2 | --- openlp/core/common/httputils.py 2019-02-14 15:09:09 +0000 |
3 | +++ openlp/core/common/httputils.py 2019-02-16 08:58:10 +0000 |
4 | @@ -27,12 +27,16 @@ |
5 | import sys |
6 | import time |
7 | from random import randint |
8 | +from tempfile import gettempdir |
9 | |
10 | import requests |
11 | +from PyQt5 import QtCore |
12 | |
13 | from openlp.core.common import trace_error_handler |
14 | +from openlp.core.common.path import Path |
15 | from openlp.core.common.registry import Registry |
16 | from openlp.core.common.settings import ProxyMode, Settings |
17 | +from openlp.core.threading import ThreadWorker |
18 | |
19 | |
20 | log = logging.getLogger(__name__ + '.__init__') |
21 | @@ -227,4 +231,46 @@ |
22 | return True |
23 | |
24 | |
25 | -__all__ = ['get_web_page'] |
26 | +class DownloadWorker(ThreadWorker): |
27 | + """ |
28 | + This worker allows a file to be downloaded in a thread |
29 | + """ |
30 | + download_failed = QtCore.pyqtSignal() |
31 | + download_succeeded = QtCore.pyqtSignal(Path) |
32 | + |
33 | + def __init__(self, base_url, file_name): |
34 | + """ |
35 | + Set up the worker object |
36 | + """ |
37 | + self._base_url = base_url |
38 | + self._file_name = file_name |
39 | + self._download_cancelled = False |
40 | + super().__init__() |
41 | + |
42 | + def start(self): |
43 | + """ |
44 | + Download the url to the temporary directory |
45 | + """ |
46 | + if self._download_cancelled: |
47 | + self.quit.emit() |
48 | + return |
49 | + try: |
50 | + dest_path = Path(gettempdir()) / 'openlp' / self._file_name |
51 | + url = f'{self._base_url}{self._file_name}' |
52 | + is_success = download_file(self, url, dest_path) |
53 | + if is_success and not self._download_cancelled: |
54 | + self.download_succeeded.emit(dest_path) |
55 | + else: |
56 | + self.download_failed.emit() |
57 | + except: # noqa |
58 | + log.exception('Unable to download %s', url) |
59 | + self.download_failed.emit() |
60 | + finally: |
61 | + self.quit.emit() |
62 | + |
63 | + @QtCore.pyqtSlot() |
64 | + def cancel_download(self): |
65 | + """ |
66 | + A slot to allow the download to be cancelled from outside of the thread |
67 | + """ |
68 | + self._download_cancelled = True |
69 | |
70 | === modified file 'openlp/core/ui/firsttimeform.py' |
71 | --- openlp/core/ui/firsttimeform.py 2019-02-14 15:09:09 +0000 |
72 | +++ openlp/core/ui/firsttimeform.py 2019-02-16 08:58:10 +0000 |
73 | @@ -22,19 +22,19 @@ |
74 | """ |
75 | This module contains the first time wizard. |
76 | """ |
77 | +import json |
78 | import logging |
79 | import time |
80 | import urllib.error |
81 | import urllib.parse |
82 | import urllib.request |
83 | -from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError |
84 | from tempfile import gettempdir |
85 | |
86 | from PyQt5 import QtCore, QtWidgets |
87 | |
88 | from openlp.core.common import clean_button_text, trace_error_handler |
89 | from openlp.core.common.applocation import AppLocation |
90 | -from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page |
91 | +from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page |
92 | from openlp.core.common.i18n import translate |
93 | from openlp.core.common.mixins import RegistryProperties |
94 | from openlp.core.common.path import Path, create_paths |
95 | @@ -43,57 +43,50 @@ |
96 | from openlp.core.lib import build_icon |
97 | from openlp.core.lib.plugin import PluginStatus |
98 | from openlp.core.lib.ui import critical_error_message_box |
99 | -from openlp.core.threading import ThreadWorker, get_thread_worker, is_thread_finished, run_thread |
100 | +from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread |
101 | from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard |
102 | +from openlp.core.ui.icons import UiIcons |
103 | |
104 | |
105 | log = logging.getLogger(__name__) |
106 | |
107 | |
108 | -class ThemeScreenshotWorker(ThreadWorker): |
109 | - """ |
110 | - This thread downloads a theme's screenshot |
111 | - """ |
112 | - screenshot_downloaded = QtCore.pyqtSignal(str, str, str) |
113 | - |
114 | - def __init__(self, themes_url, title, filename, sha256, screenshot): |
115 | - """ |
116 | - Set up the worker object |
117 | - """ |
118 | - self.was_cancelled = False |
119 | - self.themes_url = themes_url |
120 | - self.title = title |
121 | - self.filename = filename |
122 | - self.sha256 = sha256 |
123 | - self.screenshot = screenshot |
124 | - super().__init__() |
125 | - |
126 | - def start(self): |
127 | - """ |
128 | - Run the worker |
129 | - """ |
130 | - if self.was_cancelled: |
131 | - return |
132 | - try: |
133 | - download_path = Path(gettempdir()) / 'openlp' / self.screenshot |
134 | - is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot), |
135 | - download_path) |
136 | - if is_success and not self.was_cancelled: |
137 | - # Signal that the screenshot has been downloaded |
138 | - self.screenshot_downloaded.emit(self.title, self.filename, self.sha256) |
139 | - except: # noqa |
140 | - log.exception('Unable to download screenshot') |
141 | - finally: |
142 | - self.quit.emit() |
143 | - |
144 | - @QtCore.pyqtSlot(bool) |
145 | - def set_download_canceled(self, toggle): |
146 | - """ |
147 | - Externally set if the download was canceled |
148 | - |
149 | - :param toggle: Set if the download was canceled or not |
150 | - """ |
151 | - self.was_download_cancelled = toggle |
152 | +class ThemeListWidgetItem(QtWidgets.QListWidgetItem): |
153 | + """ |
154 | + Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource |
155 | + """ |
156 | + def __init__(self, themes_url, sample_theme_data, ftw, *args, **kwargs): |
157 | + super().__init__(*args, **kwargs) |
158 | + title = sample_theme_data['title'] |
159 | + thumbnail = sample_theme_data['thumbnail'] |
160 | + self.file_name = sample_theme_data['file_name'] |
161 | + self.sha256 = sample_theme_data['sha256'] |
162 | + self.setIcon(UiIcons().picture) # Set a place holder icon whilst the thumbnails download |
163 | + self.setText(title) |
164 | + self.setToolTip(title) |
165 | + worker = DownloadWorker(themes_url, thumbnail) |
166 | + worker.download_failed.connect(self._on_download_failed) |
167 | + worker.download_succeeded.connect(self._on_thumbnail_downloaded) |
168 | + thread_name = f'thumbnail_download_{thumbnail}' |
169 | + run_thread(worker, thread_name) |
170 | + ftw.thumbnail_download_threads.append(thread_name) |
171 | + |
172 | + def _on_download_failed(self): |
173 | + """ |
174 | + Set an icon to indicate that the thumbnail download has failed. |
175 | + |
176 | + :rtype: None |
177 | + """ |
178 | + self.setIcon(UiIcons().exception) |
179 | + |
180 | + def _on_thumbnail_downloaded(self, thumbnail_path): |
181 | + """ |
182 | + Load the thumbnail as the icon when it has downloaded. |
183 | + |
184 | + :param Path thumbnail_path: Path to the file to use as a thumbnail |
185 | + :rtype: None |
186 | + """ |
187 | + self.setIcon(build_icon(thumbnail_path)) |
188 | |
189 | |
190 | class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): |
191 | @@ -110,6 +103,9 @@ |
192 | self.web_access = True |
193 | self.web = '' |
194 | self.setup_ui(self) |
195 | + self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed) |
196 | + self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection) |
197 | + self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll) |
198 | |
199 | def get_next_page_id(self): |
200 | """ |
201 | @@ -144,18 +140,7 @@ |
202 | return -1 |
203 | elif self.currentId() == FirstTimePage.NoInternet: |
204 | return FirstTimePage.Progress |
205 | - elif self.currentId() == FirstTimePage.Themes: |
206 | - self.application.set_busy_cursor() |
207 | - while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): |
208 | - time.sleep(0.1) |
209 | - self.application.process_events() |
210 | - # Build the screenshot icons, as this can not be done in the thread. |
211 | - self._build_theme_screenshots() |
212 | - self.application.set_normal_cursor() |
213 | - self.theme_screenshot_threads = [] |
214 | - return self.get_next_page_id() |
215 | - else: |
216 | - return self.get_next_page_id() |
217 | + return self.get_next_page_id() |
218 | |
219 | def exec(self): |
220 | """ |
221 | @@ -172,104 +157,76 @@ |
222 | """ |
223 | self.screens = screens |
224 | self.was_cancelled = False |
225 | - self.theme_screenshot_threads = [] |
226 | + self.thumbnail_download_threads = [] |
227 | self.has_run_wizard = False |
228 | |
229 | - self.themes_list_widget.itemChanged.connect(self.on_theme_selected) |
230 | - |
231 | def _download_index(self): |
232 | """ |
233 | Download the configuration file and kick off the theme screenshot download threads |
234 | """ |
235 | # check to see if we have web access |
236 | self.web_access = False |
237 | - self.config = ConfigParser() |
238 | + self.config = '' |
239 | + web_config = None |
240 | user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() |
241 | self.application.process_events() |
242 | try: |
243 | - web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'), |
244 | + web_config = get_web_page('{host}{name}'.format(host=self.web, name='download_3.0.json'), |
245 | headers={'User-Agent': user_agent}) |
246 | except ConnectionError: |
247 | QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'), |
248 | translate('OpenLP.FirstTimeWizard', 'There was a network error attempting ' |
249 | 'to connect to retrieve initial configuration information'), |
250 | QtWidgets.QMessageBox.Ok) |
251 | - web_config = False |
252 | - if web_config: |
253 | - try: |
254 | - self.config.read_string(web_config) |
255 | - self.web = self.config.get('general', 'base url') |
256 | - self.songs_url = self.web + self.config.get('songs', 'directory') + '/' |
257 | - self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' |
258 | - self.themes_url = self.web + self.config.get('themes', 'directory') + '/' |
259 | - self.web_access = True |
260 | - except (NoSectionError, NoOptionError, MissingSectionHeaderError): |
261 | - log.debug('A problem occurred while parsing the downloaded config file') |
262 | - trace_error_handler(log) |
263 | + if web_config and self._parse_config(web_config): |
264 | + self.web_access = True |
265 | self.application.process_events() |
266 | self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...') |
267 | - if self.has_run_wizard: |
268 | - self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) |
269 | - self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) |
270 | - self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active()) |
271 | - self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) |
272 | - self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) |
273 | - self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) |
274 | - self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) |
275 | - self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) |
276 | self.application.set_normal_cursor() |
277 | - # Sort out internet access for downloads |
278 | - if self.web_access: |
279 | - songs = self.config.get('songs', 'languages') |
280 | - songs = songs.split(',') |
281 | - for song in songs: |
282 | + |
283 | + def _parse_config(self, web_config): |
284 | + try: |
285 | + config = json.loads(web_config) |
286 | + meta = config['_meta'] |
287 | + self.web = meta['base_url'] |
288 | + self.songs_url = self.web + meta['songs_dir'] + '/' |
289 | + self.bibles_url = self.web + meta['bibles_dir'] + '/' |
290 | + self.themes_url = self.web + meta['themes_dir'] + '/' |
291 | + for song in config['songs'].values(): |
292 | self.application.process_events() |
293 | - title = self.config.get('songs_{song}'.format(song=song), 'title') |
294 | - filename = self.config.get('songs_{song}'.format(song=song), 'filename') |
295 | - sha256 = self.config.get('songs_{song}'.format(song=song), 'sha256', fallback='') |
296 | - item = QtWidgets.QListWidgetItem(title, self.songs_list_widget) |
297 | - item.setData(QtCore.Qt.UserRole, (filename, sha256)) |
298 | + item = QtWidgets.QListWidgetItem(song['title'], self.songs_list_widget) |
299 | + item.setData(QtCore.Qt.UserRole, (song['file_name'], song['sha256'])) |
300 | item.setCheckState(QtCore.Qt.Unchecked) |
301 | item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) |
302 | - bible_languages = self.config.get('bibles', 'languages') |
303 | - bible_languages = bible_languages.split(',') |
304 | - for lang in bible_languages: |
305 | + for lang in config['bibles'].values(): |
306 | self.application.process_events() |
307 | - language = self.config.get('bibles_{lang}'.format(lang=lang), 'title') |
308 | - lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [language]) |
309 | - bibles = self.config.get('bibles_{lang}'.format(lang=lang), 'translations') |
310 | - bibles = bibles.split(',') |
311 | - for bible in bibles: |
312 | + lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [lang['title']]) |
313 | + for translation in lang['translations'].values(): |
314 | self.application.process_events() |
315 | - title = self.config.get('bible_{bible}'.format(bible=bible), 'title') |
316 | - filename = self.config.get('bible_{bible}'.format(bible=bible), 'filename') |
317 | - sha256 = self.config.get('bible_{bible}'.format(bible=bible), 'sha256', fallback='') |
318 | - item = QtWidgets.QTreeWidgetItem(lang_item, [title]) |
319 | - item.setData(0, QtCore.Qt.UserRole, (filename, sha256)) |
320 | + item = QtWidgets.QTreeWidgetItem(lang_item, [translation['title']]) |
321 | + item.setData(0, QtCore.Qt.UserRole, (translation['file_name'], translation['sha256'])) |
322 | item.setCheckState(0, QtCore.Qt.Unchecked) |
323 | item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) |
324 | self.bibles_tree_widget.expandAll() |
325 | self.application.process_events() |
326 | - # Download the theme screenshots |
327 | - themes = self.config.get('themes', 'files').split(',') |
328 | - for theme in themes: |
329 | - title = self.config.get('theme_{theme}'.format(theme=theme), 'title') |
330 | - filename = self.config.get('theme_{theme}'.format(theme=theme), 'filename') |
331 | - sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='') |
332 | - screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') |
333 | - worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot) |
334 | - worker.screenshot_downloaded.connect(self.on_screenshot_downloaded) |
335 | - thread_name = 'theme_screenshot_{title}'.format(title=title) |
336 | - run_thread(worker, thread_name) |
337 | - self.theme_screenshot_threads.append(thread_name) |
338 | + for theme in config['themes'].values(): |
339 | + ThemeListWidgetItem(self.themes_url, theme, self, self.themes_list_widget) |
340 | self.application.process_events() |
341 | + except Exception: |
342 | + log.exception('Unable to parse sample config file %s', web_config) |
343 | + critical_error_message_box( |
344 | + translate('OpenLP.FirstTimeWizard', 'Invalid index file'), |
345 | + translate('OpenLP.FirstTimeWizard', 'OpenLP was unable to read the resource index file. ' |
346 | + 'Please try again later.')) |
347 | + return False |
348 | + return True |
349 | |
350 | def set_defaults(self): |
351 | """ |
352 | Set up display at start of theme edit. |
353 | """ |
354 | self.restart() |
355 | - self.web = 'http://openlp.org/files/frw/' |
356 | + self.web = 'https://get.openlp.org/ftw/' |
357 | self.cancel_button.clicked.connect(self.on_cancel_button_clicked) |
358 | self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked) |
359 | self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked) |
360 | @@ -282,9 +239,18 @@ |
361 | create_paths(Path(gettempdir(), 'openlp')) |
362 | self.theme_combo_box.clear() |
363 | if self.has_run_wizard: |
364 | + self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) |
365 | + self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) |
366 | + self.presentation_check_box.setChecked( |
367 | + self.plugin_manager.get_plugin_by_name('presentations').is_active()) |
368 | + self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active()) |
369 | + self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) |
370 | + self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active()) |
371 | + self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active()) |
372 | + self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active()) |
373 | # Add any existing themes to list. |
374 | - for theme in self.theme_manager.get_themes(): |
375 | - self.theme_combo_box.addItem(theme) |
376 | + self.theme_combo_box.insertSeparator(0) |
377 | + self.theme_combo_box.addItems(sorted(self.theme_manager.get_themes())) |
378 | default_theme = Settings().value('themes/global theme') |
379 | # Pre-select the current default theme. |
380 | index = self.theme_combo_box.findText(default_theme) |
381 | @@ -335,49 +301,34 @@ |
382 | Process the triggering of the cancel button. |
383 | """ |
384 | self.was_cancelled = True |
385 | - if self.theme_screenshot_threads: |
386 | - for thread_name in self.theme_screenshot_threads: |
387 | + if self.thumbnail_download_threads: # TODO: Use main thread list |
388 | + for thread_name in self.thumbnail_download_threads: |
389 | worker = get_thread_worker(thread_name) |
390 | if worker: |
391 | - worker.set_download_canceled(True) |
392 | + worker.cancel_download() |
393 | # Was the thread created. |
394 | - if self.theme_screenshot_threads: |
395 | - while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): |
396 | + if self.thumbnail_download_threads: |
397 | + while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]): |
398 | time.sleep(0.1) |
399 | self.application.set_normal_cursor() |
400 | |
401 | - def on_screenshot_downloaded(self, title, filename, sha256): |
402 | - """ |
403 | - Add an item to the list when a theme has been downloaded |
404 | - |
405 | - :param title: The title of the theme |
406 | - :param filename: The filename of the theme |
407 | - """ |
408 | - self.themes_list_widget.blockSignals(True) |
409 | - item = QtWidgets.QListWidgetItem(title, self.themes_list_widget) |
410 | - item.setData(QtCore.Qt.UserRole, (filename, sha256)) |
411 | - item.setCheckState(QtCore.Qt.Unchecked) |
412 | - item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) |
413 | - self.themes_list_widget.blockSignals(False) |
414 | - |
415 | - def on_theme_selected(self, item): |
416 | - """ |
417 | - Add or remove a de/selected sample theme from the theme_combo_box |
418 | - |
419 | - :param QtWidgets.QListWidgetItem item: The item that has been de/selected |
420 | + def on_themes_list_widget_selection_changed(self): |
421 | + """ |
422 | + Update the `theme_combo_box` with the selected items |
423 | + |
424 | :rtype: None |
425 | """ |
426 | - theme_name = item.text() |
427 | - if self.theme_manager and theme_name in self.theme_manager.get_themes(): |
428 | - return True |
429 | - if item.checkState() == QtCore.Qt.Checked: |
430 | - self.theme_combo_box.addItem(theme_name) |
431 | - return True |
432 | - else: |
433 | - index = self.theme_combo_box.findText(theme_name) |
434 | - if index != -1: |
435 | - self.theme_combo_box.removeItem(index) |
436 | - return True |
437 | + existing_themes = [] |
438 | + if self.theme_manager: |
439 | + existing_themes = self.theme_manager.get_themes() |
440 | + for list_index in range(self.themes_list_widget.count()): |
441 | + item = self.themes_list_widget.item(list_index) |
442 | + if item.text() not in existing_themes: |
443 | + cbox_index = self.theme_combo_box.findText(item.text()) |
444 | + if item.isSelected() and cbox_index == -1: |
445 | + self.theme_combo_box.insertItem(0, item.text()) |
446 | + elif not item.isSelected() and cbox_index != -1: |
447 | + self.theme_combo_box.removeItem(cbox_index) |
448 | |
449 | def on_no_internet_finish_button_clicked(self): |
450 | """ |
451 | @@ -396,18 +347,6 @@ |
452 | self.was_cancelled = True |
453 | self.close() |
454 | |
455 | - def _build_theme_screenshots(self): |
456 | - """ |
457 | - This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``. |
458 | - """ |
459 | - themes = self.config.get('themes', 'files') |
460 | - themes = themes.split(',') |
461 | - for index, theme in enumerate(themes): |
462 | - screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') |
463 | - item = self.themes_list_widget.item(index) |
464 | - if item: |
465 | - item.setIcon(build_icon(Path(gettempdir(), 'openlp', screenshot))) |
466 | - |
467 | def update_progress(self, count, block_size): |
468 | """ |
469 | Calculate and display the download progress. This method is called by download_file(). |
470 | @@ -456,13 +395,9 @@ |
471 | self.max_progress += size |
472 | iterator += 1 |
473 | # Loop through the themes list and increase for each selected item |
474 | - for i in range(self.themes_list_widget.count()): |
475 | - self.application.process_events() |
476 | - item = self.themes_list_widget.item(i) |
477 | - if item.checkState() == QtCore.Qt.Checked: |
478 | - filename, sha256 = item.data(QtCore.Qt.UserRole) |
479 | - size = get_url_file_size('{path}{name}'.format(path=self.themes_url, name=filename)) |
480 | - self.max_progress += size |
481 | + for item in self.themes_list_widget.selectedItems(): |
482 | + size = get_url_file_size(f'{self.themes_url}{item.file_name}') |
483 | + self.max_progress += size |
484 | except urllib.error.URLError: |
485 | trace_error_handler(log) |
486 | critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), |
487 | @@ -579,15 +514,12 @@ |
488 | missed_files.append('Bible: {name}'.format(name=bible)) |
489 | bibles_iterator += 1 |
490 | # Download themes |
491 | - for i in range(self.themes_list_widget.count()): |
492 | - item = self.themes_list_widget.item(i) |
493 | - if item.checkState() == QtCore.Qt.Checked: |
494 | - theme, sha256 = item.data(QtCore.Qt.UserRole) |
495 | - self._increment_progress_bar(self.downloading.format(name=theme), 0) |
496 | - self.previous_size = 0 |
497 | - if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), |
498 | - themes_destination_path / theme, sha256): |
499 | - missed_files.append('Theme: {name}'.format(name=theme)) |
500 | + for item in self.themes_list_widget.selectedItems(): |
501 | + self._increment_progress_bar(self.downloading.format(name=item.file_name), 0) |
502 | + self.previous_size = 0 |
503 | + if not download_file( |
504 | + self, f'{self.themes_url}{item.file_name}', themes_destination_path / item.file_name, item.sha256): |
505 | + missed_files.append(f'Theme: {item.file_name}') |
506 | if missed_files: |
507 | file_list = '' |
508 | for entry in missed_files: |
509 | |
510 | === modified file 'openlp/core/ui/firsttimewizard.py' |
511 | --- openlp/core/ui/firsttimewizard.py 2019-02-14 15:09:09 +0000 |
512 | +++ openlp/core/ui/firsttimewizard.py 2019-02-16 08:58:10 +0000 |
513 | @@ -49,6 +49,39 @@ |
514 | Progress = 8 |
515 | |
516 | |
517 | +class ThemeListWidget(QtWidgets.QListWidget): |
518 | + """ |
519 | + Subclass a QListWidget so we can make it look better when it resizes. |
520 | + """ |
521 | + def __init__(self, *args, **kwargs): |
522 | + super().__init__(*args, **kwargs) |
523 | + self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) |
524 | + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) |
525 | + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) |
526 | + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) |
527 | + self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) |
528 | + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) |
529 | + self.setIconSize(QtCore.QSize(133, 100)) |
530 | + self.setMovement(QtWidgets.QListView.Static) |
531 | + self.setFlow(QtWidgets.QListView.LeftToRight) |
532 | + self.setProperty("isWrapping", True) |
533 | + self.setResizeMode(QtWidgets.QListView.Adjust) |
534 | + self.setViewMode(QtWidgets.QListView.IconMode) |
535 | + self.setUniformItemSizes(True) |
536 | + |
537 | + def resizeEvent(self, event): |
538 | + """ |
539 | + Resize the grid so the list looks better when its resized/ |
540 | + |
541 | + :param QtGui.QResizeEvent event: Not used |
542 | + :return: None |
543 | + """ |
544 | + nominal_width = 141 # Icon width of 133 + 4 each side |
545 | + max_items_per_row = self.viewport().width() // nominal_width or 1 # or 1 to avoid divide by 0 errors |
546 | + col_size = (self.viewport().width() - 1) / max_items_per_row |
547 | + self.setGridSize(QtCore.QSize(col_size, 140)) |
548 | + |
549 | + |
550 | class UiFirstTimeWizard(object): |
551 | """ |
552 | The UI widgets for the first time wizard. |
553 | @@ -175,27 +208,26 @@ |
554 | self.themes_page = QtWidgets.QWizardPage() |
555 | self.themes_page.setObjectName('themes_page') |
556 | self.themes_layout = QtWidgets.QVBoxLayout(self.themes_page) |
557 | - self.themes_layout.setContentsMargins(20, 50, 20, 60) |
558 | self.themes_layout.setObjectName('themes_layout') |
559 | - self.themes_list_widget = QtWidgets.QListWidget(self.themes_page) |
560 | - self.themes_list_widget.setViewMode(QtWidgets.QListView.IconMode) |
561 | - self.themes_list_widget.setMovement(QtWidgets.QListView.Static) |
562 | - self.themes_list_widget.setFlow(QtWidgets.QListView.LeftToRight) |
563 | - self.themes_list_widget.setSpacing(4) |
564 | - self.themes_list_widget.setUniformItemSizes(True) |
565 | - self.themes_list_widget.setIconSize(QtCore.QSize(133, 100)) |
566 | - self.themes_list_widget.setWrapping(False) |
567 | - self.themes_list_widget.setObjectName('themes_list_widget') |
568 | + self.themes_list_widget = ThemeListWidget(self.themes_page) |
569 | self.themes_layout.addWidget(self.themes_list_widget) |
570 | + self.theme_options_layout = QtWidgets.QHBoxLayout() |
571 | self.default_theme_layout = QtWidgets.QHBoxLayout() |
572 | self.theme_label = QtWidgets.QLabel(self.themes_page) |
573 | self.default_theme_layout.addWidget(self.theme_label) |
574 | self.theme_combo_box = QtWidgets.QComboBox(self.themes_page) |
575 | self.theme_combo_box.setEditable(False) |
576 | - self.theme_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert) |
577 | - self.theme_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) |
578 | - self.default_theme_layout.addWidget(self.theme_combo_box) |
579 | - self.themes_layout.addLayout(self.default_theme_layout) |
580 | + self.default_theme_layout.addWidget(self.theme_combo_box, stretch=1) |
581 | + self.theme_options_layout.addLayout(self.default_theme_layout, stretch=1) |
582 | + self.select_buttons_layout = QtWidgets.QHBoxLayout() |
583 | + self.themes_select_all_button = QtWidgets.QToolButton(self.themes_page) |
584 | + self.themes_select_all_button.setIcon(UiIcons().select_all) |
585 | + self.select_buttons_layout.addWidget(self.themes_select_all_button, stretch=1, alignment=QtCore.Qt.AlignRight) |
586 | + self.themes_deselect_all_button = QtWidgets.QToolButton(self.themes_page) |
587 | + self.themes_deselect_all_button.setIcon(UiIcons().select_none) |
588 | + self.select_buttons_layout.addWidget(self.themes_deselect_all_button) |
589 | + self.theme_options_layout.addLayout(self.select_buttons_layout, stretch=1) |
590 | + self.themes_layout.addLayout(self.theme_options_layout) |
591 | first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page) |
592 | # Progress page |
593 | self.progress_page = QtWidgets.QWizardPage() |
594 | @@ -271,9 +303,12 @@ |
595 | self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) |
596 | self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) |
597 | self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.')) |
598 | + # Themes Page |
599 | self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes')) |
600 | self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.')) |
601 | - self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Select default theme:')) |
602 | + self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Default theme:')) |
603 | + self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all')) |
604 | + self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all')) |
605 | self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring')) |
606 | self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded ' |
607 | 'and OpenLP is configured.')) |
608 | |
609 | === modified file 'openlp/core/ui/icons.py' |
610 | --- openlp/core/ui/icons.py 2019-02-14 15:09:09 +0000 |
611 | +++ openlp/core/ui/icons.py 2019-02-16 08:58:10 +0000 |
612 | @@ -138,6 +138,8 @@ |
613 | 'search_plus': {'icon': 'fa.search-plus'}, |
614 | 'search_ref': {'icon': 'fa.institution'}, |
615 | 'search_text': {'icon': 'op.search-text'}, |
616 | + 'select_all': {'icon': 'fa.check-square-o'}, |
617 | + 'select_none': {'icon': 'fa.square-o'}, |
618 | 'settings': {'icon': 'fa.cogs'}, |
619 | 'shortcuts': {'icon': 'fa.wrench'}, |
620 | 'song_usage': {'icon': 'fa.line-chart'}, |
621 | |
622 | === modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py' |
623 | --- tests/functional/openlp_core/ui/test_firsttimeform.py 2019-02-14 15:09:09 +0000 |
624 | +++ tests/functional/openlp_core/ui/test_firsttimeform.py 2019-02-16 08:58:10 +0000 |
625 | @@ -25,40 +25,69 @@ |
626 | import os |
627 | import tempfile |
628 | from unittest import TestCase |
629 | -from unittest.mock import MagicMock, call, patch |
630 | +from unittest.mock import MagicMock, call, patch, DEFAULT |
631 | |
632 | from openlp.core.common.path import Path |
633 | from openlp.core.common.registry import Registry |
634 | -from openlp.core.ui.firsttimeform import FirstTimeForm |
635 | +from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem |
636 | from tests.helpers.testmixin import TestMixin |
637 | |
638 | |
639 | -FAKE_CONFIG = """ |
640 | -[general] |
641 | -base url = http://example.com/frw/ |
642 | -[songs] |
643 | -directory = songs |
644 | -[bibles] |
645 | -directory = bibles |
646 | -[themes] |
647 | -directory = themes |
648 | -""" |
649 | - |
650 | -FAKE_BROKEN_CONFIG = """ |
651 | -[general] |
652 | -base url = http://example.com/frw/ |
653 | -[songs] |
654 | -directory = songs |
655 | -[bibles] |
656 | -directory = bibles |
657 | -""" |
658 | - |
659 | -FAKE_INVALID_CONFIG = """ |
660 | -<html> |
661 | -<head><title>This is not a config file</title></head> |
662 | -<body>Some text</body> |
663 | -</html> |
664 | -""" |
665 | +INVALID_CONFIG = """ |
666 | +{ |
667 | + "_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json", |
668 | + "_meta": { |
669 | +} |
670 | +""" |
671 | + |
672 | + |
673 | +class TestThemeListWidgetItem(TestCase): |
674 | + """ |
675 | + Test the :class:`ThemeListWidgetItem` class |
676 | + """ |
677 | + def setUp(self): |
678 | + self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash', |
679 | + 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'} |
680 | + download_worker_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker') |
681 | + self.addCleanup(download_worker_patcher.stop) |
682 | + self.mocked_download_worker = download_worker_patcher.start() |
683 | + run_thread_patcher = patch('openlp.core.ui.firsttimeform.run_thread') |
684 | + self.addCleanup(run_thread_patcher.stop) |
685 | + self.mocked_run_thread = run_thread_patcher.start() |
686 | + |
687 | + def test_init_sample_data(self): |
688 | + """ |
689 | + Test that the theme data is loaded correctly in to a ThemeListWidgetItem object when instantiated |
690 | + """ |
691 | + # GIVEN: A sample theme dictanary object |
692 | + # WHEN: Creating an instance of `ThemeListWidgetItem` |
693 | + instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) |
694 | + |
695 | + # THEN: The data should have been set correctly |
696 | + assert instance.file_name == 'BlueBurst.otz' |
697 | + assert instance.sha256 == 'sha_256_hash' |
698 | + assert instance.text() == 'Blue Burst' |
699 | + assert instance.toolTip() == 'Blue Burst' |
700 | + self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png') |
701 | + |
702 | + def test_init_download_worker(self): |
703 | + """ |
704 | + Test that the `DownloadWorker` worker is set up correctly and that the thread is started. |
705 | + """ |
706 | + # GIVEN: A sample theme dictanary object |
707 | + mocked_ftw = MagicMock(spec=FirstTimeForm) |
708 | + mocked_ftw.thumbnail_download_threads = [] |
709 | + |
710 | + # WHEN: Creating an instance of `ThemeListWidgetItem` |
711 | + instance = ThemeListWidgetItem('url', self.sample_theme_data, mocked_ftw) |
712 | + |
713 | + # THEN: The `DownloadWorker` should have been set up with the appropriate data |
714 | + self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png') |
715 | + self.mocked_download_worker.download_failed.connect.called_once_with(instance._on_download_failed()) |
716 | + self.mocked_download_worker.download_succeeded.connect.called_once_with(instance._on_thumbnail_downloaded) |
717 | + self.mocked_run_thread.assert_called_once_with( |
718 | + self.mocked_download_worker(), 'thumbnail_download_BlueBurst.png') |
719 | + assert mocked_ftw.thumbnail_download_threads == ['thumbnail_download_BlueBurst.png'] |
720 | |
721 | |
722 | class TestFirstTimeForm(TestCase, TestMixin): |
723 | @@ -92,7 +121,7 @@ |
724 | assert expected_screens == frw.screens, 'The screens should be correct' |
725 | assert frw.web_access is True, 'The default value of self.web_access should be True' |
726 | assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False' |
727 | - assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty' |
728 | + assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty' |
729 | assert frw.has_run_wizard is False, 'has_run_wizard should be False' |
730 | |
731 | def test_set_defaults(self): |
732 | @@ -109,6 +138,7 @@ |
733 | patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \ |
734 | patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \ |
735 | patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \ |
736 | + patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \ |
737 | patch.object(Registry, 'register_function') as mocked_register_function, \ |
738 | patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \ |
739 | patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \ |
740 | @@ -122,7 +152,7 @@ |
741 | |
742 | # THEN: The default values should have been set |
743 | mocked_restart.assert_called_once() |
744 | - assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set' |
745 | + assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set' |
746 | mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked) |
747 | mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with( |
748 | frw.on_no_internet_finish_button_clicked) |
749 | @@ -134,6 +164,7 @@ |
750 | mocked_create_paths.assert_called_once_with(Path('temp', 'openlp')) |
751 | mocked_theme_combo_box.clear.assert_called_once() |
752 | mocked_theme_manager.assert_not_called() |
753 | + mocked_songs_check_box.assert_not_called() |
754 | |
755 | def test_set_defaults_rerun(self): |
756 | """ |
757 | @@ -150,12 +181,17 @@ |
758 | patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \ |
759 | patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \ |
760 | patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \ |
761 | + patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT, |
762 | + image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT, |
763 | + song_usage_check_box=DEFAULT, alert_check_box=DEFAULT), \ |
764 | patch.object(Registry, 'register_function') as mocked_register_function, \ |
765 | patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \ |
766 | patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \ |
767 | patch('openlp.core.ui.firsttimeform.create_paths') as mocked_create_paths, \ |
768 | patch.object(frw.application, 'set_normal_cursor'): |
769 | - mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['a', 'b', 'c']}) |
770 | + mocked_plugin_manager = MagicMock() |
771 | + mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['b', 'a', 'c']}) |
772 | + Registry().register('plugin_manager', mocked_plugin_manager) |
773 | Registry().register('theme_manager', mocked_theme_manager) |
774 | |
775 | # WHEN: The set_defaults() method is run |
776 | @@ -163,7 +199,7 @@ |
777 | |
778 | # THEN: The default values should have been set |
779 | mocked_restart.assert_called_once() |
780 | - assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set' |
781 | + assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set' |
782 | mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked) |
783 | mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with( |
784 | frw.on_no_internet_finish_button_clicked) |
785 | @@ -173,9 +209,13 @@ |
786 | mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')]) |
787 | mocked_gettempdir.assert_called_once() |
788 | mocked_create_paths.assert_called_once_with(Path('temp', 'openlp')) |
789 | - mocked_theme_manager.assert_not_called() |
790 | + mocked_theme_manager.get_themes.assert_called_once() |
791 | mocked_theme_combo_box.clear.assert_called_once() |
792 | - mocked_theme_combo_box.addItem.assert_has_calls([call('a'), call('b'), call('c')]) |
793 | + mocked_plugin_manager.get_plugin_by_name.assert_has_calls( |
794 | + [call('songs'), call('bibles'), call('presentations'), call('images'), call('media'), call('custom'), |
795 | + call('songusage'), call('alerts')], any_order=True) |
796 | + mocked_plugin_manager.get_plugin_by_name.assert_has_calls([call().is_active()] * 8, any_order=True) |
797 | + mocked_theme_combo_box.addItems.assert_called_once_with(['a', 'b', 'c']) |
798 | mocked_theme_combo_box.findText.assert_called_once_with('Default Theme') |
799 | mocked_theme_combo_box.setCurrentIndex(3) |
800 | |
801 | @@ -192,7 +232,7 @@ |
802 | mocked_is_thread_finished.side_effect = [False, True] |
803 | frw = FirstTimeForm(None) |
804 | frw.initialize(MagicMock()) |
805 | - frw.theme_screenshot_threads = ['test_thread'] |
806 | + frw.thumbnail_download_threads = ['test_thread'] |
807 | with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: |
808 | |
809 | # WHEN: on_cancel_button_clicked() is called |
810 | @@ -201,43 +241,26 @@ |
811 | # THEN: The right things should be called in the right order |
812 | assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True' |
813 | mocked_get_thread_worker.assert_called_once_with('test_thread') |
814 | - mocked_worker.set_download_canceled.assert_called_with(True) |
815 | + mocked_worker.cancel_download.assert_called_once() |
816 | mocked_is_thread_finished.assert_called_with('test_thread') |
817 | assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice' |
818 | mocked_time.sleep.assert_called_once_with(0.1) |
819 | mocked_set_normal_cursor.assert_called_once_with() |
820 | |
821 | - def test_broken_config(self): |
822 | - """ |
823 | - Test if we can handle an config file with missing data |
824 | - """ |
825 | - # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked broken config file |
826 | - with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: |
827 | - first_time_form = FirstTimeForm(None) |
828 | - first_time_form.initialize(MagicMock()) |
829 | - mocked_get_web_page.return_value = FAKE_BROKEN_CONFIG |
830 | - |
831 | - # WHEN: The First Time Wizard is downloads the config file |
832 | - first_time_form._download_index() |
833 | - |
834 | - # THEN: The First Time Form should not have web access |
835 | - assert first_time_form.web_access is False, 'There should not be web access with a broken config file' |
836 | - |
837 | - def test_invalid_config(self): |
838 | - """ |
839 | - Test if we can handle an config file in invalid format |
840 | - """ |
841 | - # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked invalid config file |
842 | - with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: |
843 | - first_time_form = FirstTimeForm(None) |
844 | - first_time_form.initialize(MagicMock()) |
845 | - mocked_get_web_page.return_value = FAKE_INVALID_CONFIG |
846 | - |
847 | - # WHEN: The First Time Wizard is downloads the config file |
848 | - first_time_form._download_index() |
849 | - |
850 | - # THEN: The First Time Form should not have web access |
851 | - assert first_time_form.web_access is False, 'There should not be web access with an invalid config file' |
852 | + @patch('openlp.core.ui.firsttimeform.critical_error_message_box') |
853 | + def test__parse_config_invalid_config(self, mocked_critical_error_message_box): |
854 | + """ |
855 | + Test `FirstTimeForm._parse_config` when called with invalid data |
856 | + """ |
857 | + # GIVEN: An instance of `FirstTimeForm` |
858 | + first_time_form = FirstTimeForm(None) |
859 | + |
860 | + # WHEN: Calling _parse_config with a string containing invalid data |
861 | + result = first_time_form._parse_config(INVALID_CONFIG) |
862 | + |
863 | + # THEN: _parse_data should return False and the user should have should have been informed. |
864 | + assert result is False |
865 | + mocked_critical_error_message_box.assert_called_once() |
866 | |
867 | @patch('openlp.core.ui.firsttimeform.get_web_page') |
868 | @patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox') |
869 | |
870 | === added file 'tests/interfaces/openlp_core/ui/test_firsttimeform.py' |
871 | --- tests/interfaces/openlp_core/ui/test_firsttimeform.py 1970-01-01 00:00:00 +0000 |
872 | +++ tests/interfaces/openlp_core/ui/test_firsttimeform.py 2019-02-16 08:58:10 +0000 |
873 | @@ -0,0 +1,88 @@ |
874 | +# -*- coding: utf-8 -*- |
875 | +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 |
876 | + |
877 | +############################################################################### |
878 | +# OpenLP - Open Source Lyrics Projection # |
879 | +# --------------------------------------------------------------------------- # |
880 | +# Copyright (c) 2008-2019 OpenLP Developers # |
881 | +# --------------------------------------------------------------------------- # |
882 | +# This program is free software; you can redistribute it and/or modify it # |
883 | +# under the terms of the GNU General Public License as published by the Free # |
884 | +# Software Foundation; version 2 of the License. # |
885 | +# # |
886 | +# This program is distributed in the hope that it will be useful, but WITHOUT # |
887 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # |
888 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # |
889 | +# more details. # |
890 | +# # |
891 | +# You should have received a copy of the GNU General Public License along # |
892 | +# with this program; if not, write to the Free Software Foundation, Inc., 59 # |
893 | +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # |
894 | +############################################################################### |
895 | +""" |
896 | +Package to test the openlp.core.ui.firsttimeform package. |
897 | +""" |
898 | +from unittest import TestCase |
899 | +from unittest.mock import MagicMock, call, patch |
900 | + |
901 | +from openlp.core.common.path import Path |
902 | +from openlp.core.common.registry import Registry |
903 | +from openlp.core.ui.firsttimeform import ThemeListWidgetItem |
904 | +from openlp.core.ui.icons import UiIcons |
905 | +from tests.helpers.testmixin import TestMixin |
906 | + |
907 | + |
908 | +class TestThemeListWidgetItem(TestCase, TestMixin): |
909 | + def setUp(self): |
910 | + self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash', |
911 | + 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'} |
912 | + Registry.create() |
913 | + self.registry = Registry() |
914 | + mocked_app = MagicMock() |
915 | + mocked_app.worker_threads = {} |
916 | + Registry().register('application', mocked_app) |
917 | + self.setup_application() |
918 | + |
919 | + move_to_thread_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker.moveToThread') |
920 | + self.addCleanup(move_to_thread_patcher.stop) |
921 | + move_to_thread_patcher.start() |
922 | + set_icon_patcher = patch('openlp.core.ui.firsttimeform.ThemeListWidgetItem.setIcon') |
923 | + self.addCleanup(set_icon_patcher.stop) |
924 | + self.mocked_set_icon = set_icon_patcher.start() |
925 | + q_thread_patcher = patch('openlp.core.ui.firsttimeform.QtCore.QThread') |
926 | + self.addCleanup(q_thread_patcher.stop) |
927 | + q_thread_patcher.start() |
928 | + |
929 | + def test_failed_download(self): |
930 | + """ |
931 | + Test that icon get set to indicate a failure when `DownloadWorker` emits the download_failed signal |
932 | + """ |
933 | + # GIVEN: An instance of `DownloadWorker` |
934 | + instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue |
935 | + worker_threads = Registry().get('application').worker_threads |
936 | + worker = worker_threads['thumbnail_download_BlueBurst.png']['worker'] |
937 | + |
938 | + # WHEN: `DownloadWorker` emits the `download_failed` signal |
939 | + worker.download_failed.emit() |
940 | + |
941 | + # THEN: Then the initial loading icon should have been replaced by the exception icon |
942 | + self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(UiIcons().exception)]) |
943 | + |
944 | + @patch('openlp.core.ui.firsttimeform.build_icon') |
945 | + def test_successful_download(self, mocked_build_icon): |
946 | + """ |
947 | + Test that the downloaded thumbnail is set as the icon when `DownloadWorker` emits the `download_succeeded` |
948 | + signal |
949 | + """ |
950 | + # GIVEN: An instance of `DownloadWorker` |
951 | + instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue |
952 | + worker_threads = Registry().get('application').worker_threads |
953 | + worker = worker_threads['thumbnail_download_BlueBurst.png']['worker'] |
954 | + test_path = Path('downlaoded', 'file') |
955 | + |
956 | + # WHEN: `DownloadWorker` emits the `download_succeeded` signal |
957 | + worker.download_succeeded.emit(test_path) |
958 | + |
959 | + # THEN: An icon should have been built from the downloaded file and used to replace the loading icon |
960 | + mocked_build_icon.assert_called_once_with(test_path) |
961 | + self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(mocked_build_icon())]) |
Linux tests passed!