Merge lp:~phill-ridout/openlp/ftw-json-theme-list into lp:openlp

Proposed by Phill
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
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

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
Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

2853. By Phill

minor changes

2854. By Phill

Fixes

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py 2019-02-14 15:09:09 +0000
+++ openlp/core/common/httputils.py 2019-02-16 08:58:10 +0000
@@ -27,12 +27,16 @@
27import sys27import sys
28import time28import time
29from random import randint29from random import randint
30from tempfile import gettempdir
3031
31import requests32import requests
33from PyQt5 import QtCore
3234
33from openlp.core.common import trace_error_handler35from openlp.core.common import trace_error_handler
36from openlp.core.common.path import Path
34from openlp.core.common.registry import Registry37from openlp.core.common.registry import Registry
35from openlp.core.common.settings import ProxyMode, Settings38from openlp.core.common.settings import ProxyMode, Settings
39from openlp.core.threading import ThreadWorker
3640
3741
38log = logging.getLogger(__name__ + '.__init__')42log = logging.getLogger(__name__ + '.__init__')
@@ -227,4 +231,46 @@
227 return True231 return True
228232
229233
230__all__ = ['get_web_page']234class DownloadWorker(ThreadWorker):
235 """
236 This worker allows a file to be downloaded in a thread
237 """
238 download_failed = QtCore.pyqtSignal()
239 download_succeeded = QtCore.pyqtSignal(Path)
240
241 def __init__(self, base_url, file_name):
242 """
243 Set up the worker object
244 """
245 self._base_url = base_url
246 self._file_name = file_name
247 self._download_cancelled = False
248 super().__init__()
249
250 def start(self):
251 """
252 Download the url to the temporary directory
253 """
254 if self._download_cancelled:
255 self.quit.emit()
256 return
257 try:
258 dest_path = Path(gettempdir()) / 'openlp' / self._file_name
259 url = f'{self._base_url}{self._file_name}'
260 is_success = download_file(self, url, dest_path)
261 if is_success and not self._download_cancelled:
262 self.download_succeeded.emit(dest_path)
263 else:
264 self.download_failed.emit()
265 except: # noqa
266 log.exception('Unable to download %s', url)
267 self.download_failed.emit()
268 finally:
269 self.quit.emit()
270
271 @QtCore.pyqtSlot()
272 def cancel_download(self):
273 """
274 A slot to allow the download to be cancelled from outside of the thread
275 """
276 self._download_cancelled = True
231277
=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py 2019-02-14 15:09:09 +0000
+++ openlp/core/ui/firsttimeform.py 2019-02-16 08:58:10 +0000
@@ -22,19 +22,19 @@
22"""22"""
23This module contains the first time wizard.23This module contains the first time wizard.
24"""24"""
25import json
25import logging26import logging
26import time27import time
27import urllib.error28import urllib.error
28import urllib.parse29import urllib.parse
29import urllib.request30import urllib.request
30from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
31from tempfile import gettempdir31from tempfile import gettempdir
3232
33from PyQt5 import QtCore, QtWidgets33from PyQt5 import QtCore, QtWidgets
3434
35from openlp.core.common import clean_button_text, trace_error_handler35from openlp.core.common import clean_button_text, trace_error_handler
36from openlp.core.common.applocation import AppLocation36from openlp.core.common.applocation import AppLocation
37from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page37from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page
38from openlp.core.common.i18n import translate38from openlp.core.common.i18n import translate
39from openlp.core.common.mixins import RegistryProperties39from openlp.core.common.mixins import RegistryProperties
40from openlp.core.common.path import Path, create_paths40from openlp.core.common.path import Path, create_paths
@@ -43,57 +43,50 @@
43from openlp.core.lib import build_icon43from openlp.core.lib import build_icon
44from openlp.core.lib.plugin import PluginStatus44from openlp.core.lib.plugin import PluginStatus
45from openlp.core.lib.ui import critical_error_message_box45from openlp.core.lib.ui import critical_error_message_box
46from openlp.core.threading import ThreadWorker, get_thread_worker, is_thread_finished, run_thread46from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread
47from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard47from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard
48from openlp.core.ui.icons import UiIcons
4849
4950
50log = logging.getLogger(__name__)51log = logging.getLogger(__name__)
5152
5253
53class ThemeScreenshotWorker(ThreadWorker):54class ThemeListWidgetItem(QtWidgets.QListWidgetItem):
54 """55 """
55 This thread downloads a theme's screenshot56 Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource
56 """57 """
57 screenshot_downloaded = QtCore.pyqtSignal(str, str, str)58 def __init__(self, themes_url, sample_theme_data, ftw, *args, **kwargs):
5859 super().__init__(*args, **kwargs)
59 def __init__(self, themes_url, title, filename, sha256, screenshot):60 title = sample_theme_data['title']
60 """61 thumbnail = sample_theme_data['thumbnail']
61 Set up the worker object62 self.file_name = sample_theme_data['file_name']
62 """63 self.sha256 = sample_theme_data['sha256']
63 self.was_cancelled = False64 self.setIcon(UiIcons().picture) # Set a place holder icon whilst the thumbnails download
64 self.themes_url = themes_url65 self.setText(title)
65 self.title = title66 self.setToolTip(title)
66 self.filename = filename67 worker = DownloadWorker(themes_url, thumbnail)
67 self.sha256 = sha25668 worker.download_failed.connect(self._on_download_failed)
68 self.screenshot = screenshot69 worker.download_succeeded.connect(self._on_thumbnail_downloaded)
69 super().__init__()70 thread_name = f'thumbnail_download_{thumbnail}'
7071 run_thread(worker, thread_name)
71 def start(self):72 ftw.thumbnail_download_threads.append(thread_name)
72 """73
73 Run the worker74 def _on_download_failed(self):
74 """75 """
75 if self.was_cancelled:76 Set an icon to indicate that the thumbnail download has failed.
76 return77
77 try:78 :rtype: None
78 download_path = Path(gettempdir()) / 'openlp' / self.screenshot79 """
79 is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot),80 self.setIcon(UiIcons().exception)
80 download_path)81
81 if is_success and not self.was_cancelled:82 def _on_thumbnail_downloaded(self, thumbnail_path):
82 # Signal that the screenshot has been downloaded83 """
83 self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)84 Load the thumbnail as the icon when it has downloaded.
84 except: # noqa85
85 log.exception('Unable to download screenshot')86 :param Path thumbnail_path: Path to the file to use as a thumbnail
86 finally:87 :rtype: None
87 self.quit.emit()88 """
8889 self.setIcon(build_icon(thumbnail_path))
89 @QtCore.pyqtSlot(bool)
90 def set_download_canceled(self, toggle):
91 """
92 Externally set if the download was canceled
93
94 :param toggle: Set if the download was canceled or not
95 """
96 self.was_download_cancelled = toggle
9790
9891
99class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):92class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
@@ -110,6 +103,9 @@
110 self.web_access = True103 self.web_access = True
111 self.web = ''104 self.web = ''
112 self.setup_ui(self)105 self.setup_ui(self)
106 self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed)
107 self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection)
108 self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll)
113109
114 def get_next_page_id(self):110 def get_next_page_id(self):
115 """111 """
@@ -144,18 +140,7 @@
144 return -1140 return -1
145 elif self.currentId() == FirstTimePage.NoInternet:141 elif self.currentId() == FirstTimePage.NoInternet:
146 return FirstTimePage.Progress142 return FirstTimePage.Progress
147 elif self.currentId() == FirstTimePage.Themes:143 return self.get_next_page_id()
148 self.application.set_busy_cursor()
149 while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
150 time.sleep(0.1)
151 self.application.process_events()
152 # Build the screenshot icons, as this can not be done in the thread.
153 self._build_theme_screenshots()
154 self.application.set_normal_cursor()
155 self.theme_screenshot_threads = []
156 return self.get_next_page_id()
157 else:
158 return self.get_next_page_id()
159144
160 def exec(self):145 def exec(self):
161 """146 """
@@ -172,104 +157,76 @@
172 """157 """
173 self.screens = screens158 self.screens = screens
174 self.was_cancelled = False159 self.was_cancelled = False
175 self.theme_screenshot_threads = []160 self.thumbnail_download_threads = []
176 self.has_run_wizard = False161 self.has_run_wizard = False
177162
178 self.themes_list_widget.itemChanged.connect(self.on_theme_selected)
179
180 def _download_index(self):163 def _download_index(self):
181 """164 """
182 Download the configuration file and kick off the theme screenshot download threads165 Download the configuration file and kick off the theme screenshot download threads
183 """166 """
184 # check to see if we have web access167 # check to see if we have web access
185 self.web_access = False168 self.web_access = False
186 self.config = ConfigParser()169 self.config = ''
170 web_config = None
187 user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()171 user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
188 self.application.process_events()172 self.application.process_events()
189 try:173 try:
190 web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'),174 web_config = get_web_page('{host}{name}'.format(host=self.web, name='download_3.0.json'),
191 headers={'User-Agent': user_agent})175 headers={'User-Agent': user_agent})
192 except ConnectionError:176 except ConnectionError:
193 QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),177 QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),
194 translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '178 translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '
195 'to connect to retrieve initial configuration information'),179 'to connect to retrieve initial configuration information'),
196 QtWidgets.QMessageBox.Ok)180 QtWidgets.QMessageBox.Ok)
197 web_config = False181 if web_config and self._parse_config(web_config):
198 if web_config:182 self.web_access = True
199 try:
200 self.config.read_string(web_config)
201 self.web = self.config.get('general', 'base url')
202 self.songs_url = self.web + self.config.get('songs', 'directory') + '/'
203 self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/'
204 self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
205 self.web_access = True
206 except (NoSectionError, NoOptionError, MissingSectionHeaderError):
207 log.debug('A problem occurred while parsing the downloaded config file')
208 trace_error_handler(log)
209 self.application.process_events()183 self.application.process_events()
210 self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')184 self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
211 if self.has_run_wizard:
212 self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
213 self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
214 self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active())
215 self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
216 self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
217 self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
218 self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
219 self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
220 self.application.set_normal_cursor()185 self.application.set_normal_cursor()
221 # Sort out internet access for downloads186
222 if self.web_access:187 def _parse_config(self, web_config):
223 songs = self.config.get('songs', 'languages')188 try:
224 songs = songs.split(',')189 config = json.loads(web_config)
225 for song in songs:190 meta = config['_meta']
191 self.web = meta['base_url']
192 self.songs_url = self.web + meta['songs_dir'] + '/'
193 self.bibles_url = self.web + meta['bibles_dir'] + '/'
194 self.themes_url = self.web + meta['themes_dir'] + '/'
195 for song in config['songs'].values():
226 self.application.process_events()196 self.application.process_events()
227 title = self.config.get('songs_{song}'.format(song=song), 'title')197 item = QtWidgets.QListWidgetItem(song['title'], self.songs_list_widget)
228 filename = self.config.get('songs_{song}'.format(song=song), 'filename')198 item.setData(QtCore.Qt.UserRole, (song['file_name'], song['sha256']))
229 sha256 = self.config.get('songs_{song}'.format(song=song), 'sha256', fallback='')
230 item = QtWidgets.QListWidgetItem(title, self.songs_list_widget)
231 item.setData(QtCore.Qt.UserRole, (filename, sha256))
232 item.setCheckState(QtCore.Qt.Unchecked)199 item.setCheckState(QtCore.Qt.Unchecked)
233 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)200 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
234 bible_languages = self.config.get('bibles', 'languages')201 for lang in config['bibles'].values():
235 bible_languages = bible_languages.split(',')
236 for lang in bible_languages:
237 self.application.process_events()202 self.application.process_events()
238 language = self.config.get('bibles_{lang}'.format(lang=lang), 'title')203 lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [lang['title']])
239 lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [language])204 for translation in lang['translations'].values():
240 bibles = self.config.get('bibles_{lang}'.format(lang=lang), 'translations')
241 bibles = bibles.split(',')
242 for bible in bibles:
243 self.application.process_events()205 self.application.process_events()
244 title = self.config.get('bible_{bible}'.format(bible=bible), 'title')206 item = QtWidgets.QTreeWidgetItem(lang_item, [translation['title']])
245 filename = self.config.get('bible_{bible}'.format(bible=bible), 'filename')207 item.setData(0, QtCore.Qt.UserRole, (translation['file_name'], translation['sha256']))
246 sha256 = self.config.get('bible_{bible}'.format(bible=bible), 'sha256', fallback='')
247 item = QtWidgets.QTreeWidgetItem(lang_item, [title])
248 item.setData(0, QtCore.Qt.UserRole, (filename, sha256))
249 item.setCheckState(0, QtCore.Qt.Unchecked)208 item.setCheckState(0, QtCore.Qt.Unchecked)
250 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)209 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
251 self.bibles_tree_widget.expandAll()210 self.bibles_tree_widget.expandAll()
252 self.application.process_events()211 self.application.process_events()
253 # Download the theme screenshots212 for theme in config['themes'].values():
254 themes = self.config.get('themes', 'files').split(',')213 ThemeListWidgetItem(self.themes_url, theme, self, self.themes_list_widget)
255 for theme in themes:
256 title = self.config.get('theme_{theme}'.format(theme=theme), 'title')
257 filename = self.config.get('theme_{theme}'.format(theme=theme), 'filename')
258 sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')
259 screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
260 worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
261 worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
262 thread_name = 'theme_screenshot_{title}'.format(title=title)
263 run_thread(worker, thread_name)
264 self.theme_screenshot_threads.append(thread_name)
265 self.application.process_events()214 self.application.process_events()
215 except Exception:
216 log.exception('Unable to parse sample config file %s', web_config)
217 critical_error_message_box(
218 translate('OpenLP.FirstTimeWizard', 'Invalid index file'),
219 translate('OpenLP.FirstTimeWizard', 'OpenLP was unable to read the resource index file. '
220 'Please try again later.'))
221 return False
222 return True
266223
267 def set_defaults(self):224 def set_defaults(self):
268 """225 """
269 Set up display at start of theme edit.226 Set up display at start of theme edit.
270 """227 """
271 self.restart()228 self.restart()
272 self.web = 'http://openlp.org/files/frw/'229 self.web = 'https://get.openlp.org/ftw/'
273 self.cancel_button.clicked.connect(self.on_cancel_button_clicked)230 self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
274 self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked)231 self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked)
275 self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked)232 self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked)
@@ -282,9 +239,18 @@
282 create_paths(Path(gettempdir(), 'openlp'))239 create_paths(Path(gettempdir(), 'openlp'))
283 self.theme_combo_box.clear()240 self.theme_combo_box.clear()
284 if self.has_run_wizard:241 if self.has_run_wizard:
242 self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
243 self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
244 self.presentation_check_box.setChecked(
245 self.plugin_manager.get_plugin_by_name('presentations').is_active())
246 self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
247 self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
248 self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
249 self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
250 self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
285 # Add any existing themes to list.251 # Add any existing themes to list.
286 for theme in self.theme_manager.get_themes():252 self.theme_combo_box.insertSeparator(0)
287 self.theme_combo_box.addItem(theme)253 self.theme_combo_box.addItems(sorted(self.theme_manager.get_themes()))
288 default_theme = Settings().value('themes/global theme')254 default_theme = Settings().value('themes/global theme')
289 # Pre-select the current default theme.255 # Pre-select the current default theme.
290 index = self.theme_combo_box.findText(default_theme)256 index = self.theme_combo_box.findText(default_theme)
@@ -335,49 +301,34 @@
335 Process the triggering of the cancel button.301 Process the triggering of the cancel button.
336 """302 """
337 self.was_cancelled = True303 self.was_cancelled = True
338 if self.theme_screenshot_threads:304 if self.thumbnail_download_threads: # TODO: Use main thread list
339 for thread_name in self.theme_screenshot_threads:305 for thread_name in self.thumbnail_download_threads:
340 worker = get_thread_worker(thread_name)306 worker = get_thread_worker(thread_name)
341 if worker:307 if worker:
342 worker.set_download_canceled(True)308 worker.cancel_download()
343 # Was the thread created.309 # Was the thread created.
344 if self.theme_screenshot_threads:310 if self.thumbnail_download_threads:
345 while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):311 while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]):
346 time.sleep(0.1)312 time.sleep(0.1)
347 self.application.set_normal_cursor()313 self.application.set_normal_cursor()
348314
349 def on_screenshot_downloaded(self, title, filename, sha256):315 def on_themes_list_widget_selection_changed(self):
350 """316 """
351 Add an item to the list when a theme has been downloaded317 Update the `theme_combo_box` with the selected items
352318
353 :param title: The title of the theme
354 :param filename: The filename of the theme
355 """
356 self.themes_list_widget.blockSignals(True)
357 item = QtWidgets.QListWidgetItem(title, self.themes_list_widget)
358 item.setData(QtCore.Qt.UserRole, (filename, sha256))
359 item.setCheckState(QtCore.Qt.Unchecked)
360 item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
361 self.themes_list_widget.blockSignals(False)
362
363 def on_theme_selected(self, item):
364 """
365 Add or remove a de/selected sample theme from the theme_combo_box
366
367 :param QtWidgets.QListWidgetItem item: The item that has been de/selected
368 :rtype: None319 :rtype: None
369 """320 """
370 theme_name = item.text()321 existing_themes = []
371 if self.theme_manager and theme_name in self.theme_manager.get_themes():322 if self.theme_manager:
372 return True323 existing_themes = self.theme_manager.get_themes()
373 if item.checkState() == QtCore.Qt.Checked:324 for list_index in range(self.themes_list_widget.count()):
374 self.theme_combo_box.addItem(theme_name)325 item = self.themes_list_widget.item(list_index)
375 return True326 if item.text() not in existing_themes:
376 else:327 cbox_index = self.theme_combo_box.findText(item.text())
377 index = self.theme_combo_box.findText(theme_name)328 if item.isSelected() and cbox_index == -1:
378 if index != -1:329 self.theme_combo_box.insertItem(0, item.text())
379 self.theme_combo_box.removeItem(index)330 elif not item.isSelected() and cbox_index != -1:
380 return True331 self.theme_combo_box.removeItem(cbox_index)
381332
382 def on_no_internet_finish_button_clicked(self):333 def on_no_internet_finish_button_clicked(self):
383 """334 """
@@ -396,18 +347,6 @@
396 self.was_cancelled = True347 self.was_cancelled = True
397 self.close()348 self.close()
398349
399 def _build_theme_screenshots(self):
400 """
401 This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``.
402 """
403 themes = self.config.get('themes', 'files')
404 themes = themes.split(',')
405 for index, theme in enumerate(themes):
406 screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
407 item = self.themes_list_widget.item(index)
408 if item:
409 item.setIcon(build_icon(Path(gettempdir(), 'openlp', screenshot)))
410
411 def update_progress(self, count, block_size):350 def update_progress(self, count, block_size):
412 """351 """
413 Calculate and display the download progress. This method is called by download_file().352 Calculate and display the download progress. This method is called by download_file().
@@ -456,13 +395,9 @@
456 self.max_progress += size395 self.max_progress += size
457 iterator += 1396 iterator += 1
458 # Loop through the themes list and increase for each selected item397 # Loop through the themes list and increase for each selected item
459 for i in range(self.themes_list_widget.count()):398 for item in self.themes_list_widget.selectedItems():
460 self.application.process_events()399 size = get_url_file_size(f'{self.themes_url}{item.file_name}')
461 item = self.themes_list_widget.item(i)400 self.max_progress += size
462 if item.checkState() == QtCore.Qt.Checked:
463 filename, sha256 = item.data(QtCore.Qt.UserRole)
464 size = get_url_file_size('{path}{name}'.format(path=self.themes_url, name=filename))
465 self.max_progress += size
466 except urllib.error.URLError:401 except urllib.error.URLError:
467 trace_error_handler(log)402 trace_error_handler(log)
468 critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),403 critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
@@ -579,15 +514,12 @@
579 missed_files.append('Bible: {name}'.format(name=bible))514 missed_files.append('Bible: {name}'.format(name=bible))
580 bibles_iterator += 1515 bibles_iterator += 1
581 # Download themes516 # Download themes
582 for i in range(self.themes_list_widget.count()):517 for item in self.themes_list_widget.selectedItems():
583 item = self.themes_list_widget.item(i)518 self._increment_progress_bar(self.downloading.format(name=item.file_name), 0)
584 if item.checkState() == QtCore.Qt.Checked:519 self.previous_size = 0
585 theme, sha256 = item.data(QtCore.Qt.UserRole)520 if not download_file(
586 self._increment_progress_bar(self.downloading.format(name=theme), 0)521 self, f'{self.themes_url}{item.file_name}', themes_destination_path / item.file_name, item.sha256):
587 self.previous_size = 0522 missed_files.append(f'Theme: {item.file_name}')
588 if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
589 themes_destination_path / theme, sha256):
590 missed_files.append('Theme: {name}'.format(name=theme))
591 if missed_files:523 if missed_files:
592 file_list = ''524 file_list = ''
593 for entry in missed_files:525 for entry in missed_files:
594526
=== modified file 'openlp/core/ui/firsttimewizard.py'
--- openlp/core/ui/firsttimewizard.py 2019-02-14 15:09:09 +0000
+++ openlp/core/ui/firsttimewizard.py 2019-02-16 08:58:10 +0000
@@ -49,6 +49,39 @@
49 Progress = 849 Progress = 8
5050
5151
52class ThemeListWidget(QtWidgets.QListWidget):
53 """
54 Subclass a QListWidget so we can make it look better when it resizes.
55 """
56 def __init__(self, *args, **kwargs):
57 super().__init__(*args, **kwargs)
58 self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
59 self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
60 self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
61 self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
62 self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
63 self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
64 self.setIconSize(QtCore.QSize(133, 100))
65 self.setMovement(QtWidgets.QListView.Static)
66 self.setFlow(QtWidgets.QListView.LeftToRight)
67 self.setProperty("isWrapping", True)
68 self.setResizeMode(QtWidgets.QListView.Adjust)
69 self.setViewMode(QtWidgets.QListView.IconMode)
70 self.setUniformItemSizes(True)
71
72 def resizeEvent(self, event):
73 """
74 Resize the grid so the list looks better when its resized/
75
76 :param QtGui.QResizeEvent event: Not used
77 :return: None
78 """
79 nominal_width = 141 # Icon width of 133 + 4 each side
80 max_items_per_row = self.viewport().width() // nominal_width or 1 # or 1 to avoid divide by 0 errors
81 col_size = (self.viewport().width() - 1) / max_items_per_row
82 self.setGridSize(QtCore.QSize(col_size, 140))
83
84
52class UiFirstTimeWizard(object):85class UiFirstTimeWizard(object):
53 """86 """
54 The UI widgets for the first time wizard.87 The UI widgets for the first time wizard.
@@ -175,27 +208,26 @@
175 self.themes_page = QtWidgets.QWizardPage()208 self.themes_page = QtWidgets.QWizardPage()
176 self.themes_page.setObjectName('themes_page')209 self.themes_page.setObjectName('themes_page')
177 self.themes_layout = QtWidgets.QVBoxLayout(self.themes_page)210 self.themes_layout = QtWidgets.QVBoxLayout(self.themes_page)
178 self.themes_layout.setContentsMargins(20, 50, 20, 60)
179 self.themes_layout.setObjectName('themes_layout')211 self.themes_layout.setObjectName('themes_layout')
180 self.themes_list_widget = QtWidgets.QListWidget(self.themes_page)212 self.themes_list_widget = ThemeListWidget(self.themes_page)
181 self.themes_list_widget.setViewMode(QtWidgets.QListView.IconMode)
182 self.themes_list_widget.setMovement(QtWidgets.QListView.Static)
183 self.themes_list_widget.setFlow(QtWidgets.QListView.LeftToRight)
184 self.themes_list_widget.setSpacing(4)
185 self.themes_list_widget.setUniformItemSizes(True)
186 self.themes_list_widget.setIconSize(QtCore.QSize(133, 100))
187 self.themes_list_widget.setWrapping(False)
188 self.themes_list_widget.setObjectName('themes_list_widget')
189 self.themes_layout.addWidget(self.themes_list_widget)213 self.themes_layout.addWidget(self.themes_list_widget)
214 self.theme_options_layout = QtWidgets.QHBoxLayout()
190 self.default_theme_layout = QtWidgets.QHBoxLayout()215 self.default_theme_layout = QtWidgets.QHBoxLayout()
191 self.theme_label = QtWidgets.QLabel(self.themes_page)216 self.theme_label = QtWidgets.QLabel(self.themes_page)
192 self.default_theme_layout.addWidget(self.theme_label)217 self.default_theme_layout.addWidget(self.theme_label)
193 self.theme_combo_box = QtWidgets.QComboBox(self.themes_page)218 self.theme_combo_box = QtWidgets.QComboBox(self.themes_page)
194 self.theme_combo_box.setEditable(False)219 self.theme_combo_box.setEditable(False)
195 self.theme_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert)220 self.default_theme_layout.addWidget(self.theme_combo_box, stretch=1)
196 self.theme_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)221 self.theme_options_layout.addLayout(self.default_theme_layout, stretch=1)
197 self.default_theme_layout.addWidget(self.theme_combo_box)222 self.select_buttons_layout = QtWidgets.QHBoxLayout()
198 self.themes_layout.addLayout(self.default_theme_layout)223 self.themes_select_all_button = QtWidgets.QToolButton(self.themes_page)
224 self.themes_select_all_button.setIcon(UiIcons().select_all)
225 self.select_buttons_layout.addWidget(self.themes_select_all_button, stretch=1, alignment=QtCore.Qt.AlignRight)
226 self.themes_deselect_all_button = QtWidgets.QToolButton(self.themes_page)
227 self.themes_deselect_all_button.setIcon(UiIcons().select_none)
228 self.select_buttons_layout.addWidget(self.themes_deselect_all_button)
229 self.theme_options_layout.addLayout(self.select_buttons_layout, stretch=1)
230 self.themes_layout.addLayout(self.theme_options_layout)
199 first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page)231 first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page)
200 # Progress page232 # Progress page
201 self.progress_page = QtWidgets.QWizardPage()233 self.progress_page = QtWidgets.QWizardPage()
@@ -271,9 +303,12 @@
271 self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.'))303 self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.'))
272 self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles'))304 self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles'))
273 self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.'))305 self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.'))
306 # Themes Page
274 self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes'))307 self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes'))
275 self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.'))308 self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.'))
276 self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Select default theme:'))309 self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Default theme:'))
310 self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all'))
311 self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all'))
277 self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring'))312 self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring'))
278 self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded '313 self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded '
279 'and OpenLP is configured.'))314 'and OpenLP is configured.'))
280315
=== modified file 'openlp/core/ui/icons.py'
--- openlp/core/ui/icons.py 2019-02-14 15:09:09 +0000
+++ openlp/core/ui/icons.py 2019-02-16 08:58:10 +0000
@@ -138,6 +138,8 @@
138 'search_plus': {'icon': 'fa.search-plus'},138 'search_plus': {'icon': 'fa.search-plus'},
139 'search_ref': {'icon': 'fa.institution'},139 'search_ref': {'icon': 'fa.institution'},
140 'search_text': {'icon': 'op.search-text'},140 'search_text': {'icon': 'op.search-text'},
141 'select_all': {'icon': 'fa.check-square-o'},
142 'select_none': {'icon': 'fa.square-o'},
141 'settings': {'icon': 'fa.cogs'},143 'settings': {'icon': 'fa.cogs'},
142 'shortcuts': {'icon': 'fa.wrench'},144 'shortcuts': {'icon': 'fa.wrench'},
143 'song_usage': {'icon': 'fa.line-chart'},145 'song_usage': {'icon': 'fa.line-chart'},
144146
=== modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py'
--- tests/functional/openlp_core/ui/test_firsttimeform.py 2019-02-14 15:09:09 +0000
+++ tests/functional/openlp_core/ui/test_firsttimeform.py 2019-02-16 08:58:10 +0000
@@ -25,40 +25,69 @@
25import os25import os
26import tempfile26import tempfile
27from unittest import TestCase27from unittest import TestCase
28from unittest.mock import MagicMock, call, patch28from unittest.mock import MagicMock, call, patch, DEFAULT
2929
30from openlp.core.common.path import Path30from openlp.core.common.path import Path
31from openlp.core.common.registry import Registry31from openlp.core.common.registry import Registry
32from openlp.core.ui.firsttimeform import FirstTimeForm32from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem
33from tests.helpers.testmixin import TestMixin33from tests.helpers.testmixin import TestMixin
3434
3535
36FAKE_CONFIG = """36INVALID_CONFIG = """
37[general]37{
38base url = http://example.com/frw/38 "_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json",
39[songs]39 "_meta": {
40directory = songs40}
41[bibles]41"""
42directory = bibles42
43[themes]43
44directory = themes44class TestThemeListWidgetItem(TestCase):
45"""45 """
4646 Test the :class:`ThemeListWidgetItem` class
47FAKE_BROKEN_CONFIG = """47 """
48[general]48 def setUp(self):
49base url = http://example.com/frw/49 self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash',
50[songs]50 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'}
51directory = songs51 download_worker_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker')
52[bibles]52 self.addCleanup(download_worker_patcher.stop)
53directory = bibles53 self.mocked_download_worker = download_worker_patcher.start()
54"""54 run_thread_patcher = patch('openlp.core.ui.firsttimeform.run_thread')
5555 self.addCleanup(run_thread_patcher.stop)
56FAKE_INVALID_CONFIG = """56 self.mocked_run_thread = run_thread_patcher.start()
57<html>57
58<head><title>This is not a config file</title></head>58 def test_init_sample_data(self):
59<body>Some text</body>59 """
60</html>60 Test that the theme data is loaded correctly in to a ThemeListWidgetItem object when instantiated
61"""61 """
62 # GIVEN: A sample theme dictanary object
63 # WHEN: Creating an instance of `ThemeListWidgetItem`
64 instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock())
65
66 # THEN: The data should have been set correctly
67 assert instance.file_name == 'BlueBurst.otz'
68 assert instance.sha256 == 'sha_256_hash'
69 assert instance.text() == 'Blue Burst'
70 assert instance.toolTip() == 'Blue Burst'
71 self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png')
72
73 def test_init_download_worker(self):
74 """
75 Test that the `DownloadWorker` worker is set up correctly and that the thread is started.
76 """
77 # GIVEN: A sample theme dictanary object
78 mocked_ftw = MagicMock(spec=FirstTimeForm)
79 mocked_ftw.thumbnail_download_threads = []
80
81 # WHEN: Creating an instance of `ThemeListWidgetItem`
82 instance = ThemeListWidgetItem('url', self.sample_theme_data, mocked_ftw)
83
84 # THEN: The `DownloadWorker` should have been set up with the appropriate data
85 self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png')
86 self.mocked_download_worker.download_failed.connect.called_once_with(instance._on_download_failed())
87 self.mocked_download_worker.download_succeeded.connect.called_once_with(instance._on_thumbnail_downloaded)
88 self.mocked_run_thread.assert_called_once_with(
89 self.mocked_download_worker(), 'thumbnail_download_BlueBurst.png')
90 assert mocked_ftw.thumbnail_download_threads == ['thumbnail_download_BlueBurst.png']
6291
6392
64class TestFirstTimeForm(TestCase, TestMixin):93class TestFirstTimeForm(TestCase, TestMixin):
@@ -92,7 +121,7 @@
92 assert expected_screens == frw.screens, 'The screens should be correct'121 assert expected_screens == frw.screens, 'The screens should be correct'
93 assert frw.web_access is True, 'The default value of self.web_access should be True'122 assert frw.web_access is True, 'The default value of self.web_access should be True'
94 assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'123 assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'
95 assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty'124 assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty'
96 assert frw.has_run_wizard is False, 'has_run_wizard should be False'125 assert frw.has_run_wizard is False, 'has_run_wizard should be False'
97126
98 def test_set_defaults(self):127 def test_set_defaults(self):
@@ -109,6 +138,7 @@
109 patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \138 patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
110 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \139 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
111 patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \140 patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \
141 patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \
112 patch.object(Registry, 'register_function') as mocked_register_function, \142 patch.object(Registry, 'register_function') as mocked_register_function, \
113 patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \143 patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \
114 patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \144 patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \
@@ -122,7 +152,7 @@
122152
123 # THEN: The default values should have been set153 # THEN: The default values should have been set
124 mocked_restart.assert_called_once()154 mocked_restart.assert_called_once()
125 assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set'155 assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
126 mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)156 mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
127 mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(157 mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
128 frw.on_no_internet_finish_button_clicked)158 frw.on_no_internet_finish_button_clicked)
@@ -134,6 +164,7 @@
134 mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))164 mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
135 mocked_theme_combo_box.clear.assert_called_once()165 mocked_theme_combo_box.clear.assert_called_once()
136 mocked_theme_manager.assert_not_called()166 mocked_theme_manager.assert_not_called()
167 mocked_songs_check_box.assert_not_called()
137168
138 def test_set_defaults_rerun(self):169 def test_set_defaults_rerun(self):
139 """170 """
@@ -150,12 +181,17 @@
150 patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \181 patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
151 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \182 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
152 patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \183 patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \
184 patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT,
185 image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT,
186 song_usage_check_box=DEFAULT, alert_check_box=DEFAULT), \
153 patch.object(Registry, 'register_function') as mocked_register_function, \187 patch.object(Registry, 'register_function') as mocked_register_function, \
154 patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \188 patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \
155 patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \189 patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \
156 patch('openlp.core.ui.firsttimeform.create_paths') as mocked_create_paths, \190 patch('openlp.core.ui.firsttimeform.create_paths') as mocked_create_paths, \
157 patch.object(frw.application, 'set_normal_cursor'):191 patch.object(frw.application, 'set_normal_cursor'):
158 mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['a', 'b', 'c']})192 mocked_plugin_manager = MagicMock()
193 mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['b', 'a', 'c']})
194 Registry().register('plugin_manager', mocked_plugin_manager)
159 Registry().register('theme_manager', mocked_theme_manager)195 Registry().register('theme_manager', mocked_theme_manager)
160196
161 # WHEN: The set_defaults() method is run197 # WHEN: The set_defaults() method is run
@@ -163,7 +199,7 @@
163199
164 # THEN: The default values should have been set200 # THEN: The default values should have been set
165 mocked_restart.assert_called_once()201 mocked_restart.assert_called_once()
166 assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set'202 assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
167 mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)203 mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
168 mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(204 mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
169 frw.on_no_internet_finish_button_clicked)205 frw.on_no_internet_finish_button_clicked)
@@ -173,9 +209,13 @@
173 mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')])209 mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')])
174 mocked_gettempdir.assert_called_once()210 mocked_gettempdir.assert_called_once()
175 mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))211 mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
176 mocked_theme_manager.assert_not_called()212 mocked_theme_manager.get_themes.assert_called_once()
177 mocked_theme_combo_box.clear.assert_called_once()213 mocked_theme_combo_box.clear.assert_called_once()
178 mocked_theme_combo_box.addItem.assert_has_calls([call('a'), call('b'), call('c')])214 mocked_plugin_manager.get_plugin_by_name.assert_has_calls(
215 [call('songs'), call('bibles'), call('presentations'), call('images'), call('media'), call('custom'),
216 call('songusage'), call('alerts')], any_order=True)
217 mocked_plugin_manager.get_plugin_by_name.assert_has_calls([call().is_active()] * 8, any_order=True)
218 mocked_theme_combo_box.addItems.assert_called_once_with(['a', 'b', 'c'])
179 mocked_theme_combo_box.findText.assert_called_once_with('Default Theme')219 mocked_theme_combo_box.findText.assert_called_once_with('Default Theme')
180 mocked_theme_combo_box.setCurrentIndex(3)220 mocked_theme_combo_box.setCurrentIndex(3)
181221
@@ -192,7 +232,7 @@
192 mocked_is_thread_finished.side_effect = [False, True]232 mocked_is_thread_finished.side_effect = [False, True]
193 frw = FirstTimeForm(None)233 frw = FirstTimeForm(None)
194 frw.initialize(MagicMock())234 frw.initialize(MagicMock())
195 frw.theme_screenshot_threads = ['test_thread']235 frw.thumbnail_download_threads = ['test_thread']
196 with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:236 with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
197237
198 # WHEN: on_cancel_button_clicked() is called238 # WHEN: on_cancel_button_clicked() is called
@@ -201,43 +241,26 @@
201 # THEN: The right things should be called in the right order241 # THEN: The right things should be called in the right order
202 assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'242 assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'
203 mocked_get_thread_worker.assert_called_once_with('test_thread')243 mocked_get_thread_worker.assert_called_once_with('test_thread')
204 mocked_worker.set_download_canceled.assert_called_with(True)244 mocked_worker.cancel_download.assert_called_once()
205 mocked_is_thread_finished.assert_called_with('test_thread')245 mocked_is_thread_finished.assert_called_with('test_thread')
206 assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'246 assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
207 mocked_time.sleep.assert_called_once_with(0.1)247 mocked_time.sleep.assert_called_once_with(0.1)
208 mocked_set_normal_cursor.assert_called_once_with()248 mocked_set_normal_cursor.assert_called_once_with()
209249
210 def test_broken_config(self):250 @patch('openlp.core.ui.firsttimeform.critical_error_message_box')
211 """251 def test__parse_config_invalid_config(self, mocked_critical_error_message_box):
212 Test if we can handle an config file with missing data252 """
213 """253 Test `FirstTimeForm._parse_config` when called with invalid data
214 # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked broken config file254 """
215 with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:255 # GIVEN: An instance of `FirstTimeForm`
216 first_time_form = FirstTimeForm(None)256 first_time_form = FirstTimeForm(None)
217 first_time_form.initialize(MagicMock())257
218 mocked_get_web_page.return_value = FAKE_BROKEN_CONFIG258 # WHEN: Calling _parse_config with a string containing invalid data
219259 result = first_time_form._parse_config(INVALID_CONFIG)
220 # WHEN: The First Time Wizard is downloads the config file260
221 first_time_form._download_index()261 # THEN: _parse_data should return False and the user should have should have been informed.
222262 assert result is False
223 # THEN: The First Time Form should not have web access263 mocked_critical_error_message_box.assert_called_once()
224 assert first_time_form.web_access is False, 'There should not be web access with a broken config file'
225
226 def test_invalid_config(self):
227 """
228 Test if we can handle an config file in invalid format
229 """
230 # GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked invalid config file
231 with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:
232 first_time_form = FirstTimeForm(None)
233 first_time_form.initialize(MagicMock())
234 mocked_get_web_page.return_value = FAKE_INVALID_CONFIG
235
236 # WHEN: The First Time Wizard is downloads the config file
237 first_time_form._download_index()
238
239 # THEN: The First Time Form should not have web access
240 assert first_time_form.web_access is False, 'There should not be web access with an invalid config file'
241264
242 @patch('openlp.core.ui.firsttimeform.get_web_page')265 @patch('openlp.core.ui.firsttimeform.get_web_page')
243 @patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox')266 @patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox')
244267
=== added file 'tests/interfaces/openlp_core/ui/test_firsttimeform.py'
--- tests/interfaces/openlp_core/ui/test_firsttimeform.py 1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_core/ui/test_firsttimeform.py 2019-02-16 08:58:10 +0000
@@ -0,0 +1,88 @@
1# -*- coding: utf-8 -*-
2# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3
4###############################################################################
5# OpenLP - Open Source Lyrics Projection #
6# --------------------------------------------------------------------------- #
7# Copyright (c) 2008-2019 OpenLP Developers #
8# --------------------------------------------------------------------------- #
9# This program is free software; you can redistribute it and/or modify it #
10# under the terms of the GNU General Public License as published by the Free #
11# Software Foundation; version 2 of the License. #
12# #
13# This program is distributed in the hope that it will be useful, but WITHOUT #
14# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
16# more details. #
17# #
18# You should have received a copy of the GNU General Public License along #
19# with this program; if not, write to the Free Software Foundation, Inc., 59 #
20# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
21###############################################################################
22"""
23Package to test the openlp.core.ui.firsttimeform package.
24"""
25from unittest import TestCase
26from unittest.mock import MagicMock, call, patch
27
28from openlp.core.common.path import Path
29from openlp.core.common.registry import Registry
30from openlp.core.ui.firsttimeform import ThemeListWidgetItem
31from openlp.core.ui.icons import UiIcons
32from tests.helpers.testmixin import TestMixin
33
34
35class TestThemeListWidgetItem(TestCase, TestMixin):
36 def setUp(self):
37 self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash',
38 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'}
39 Registry.create()
40 self.registry = Registry()
41 mocked_app = MagicMock()
42 mocked_app.worker_threads = {}
43 Registry().register('application', mocked_app)
44 self.setup_application()
45
46 move_to_thread_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker.moveToThread')
47 self.addCleanup(move_to_thread_patcher.stop)
48 move_to_thread_patcher.start()
49 set_icon_patcher = patch('openlp.core.ui.firsttimeform.ThemeListWidgetItem.setIcon')
50 self.addCleanup(set_icon_patcher.stop)
51 self.mocked_set_icon = set_icon_patcher.start()
52 q_thread_patcher = patch('openlp.core.ui.firsttimeform.QtCore.QThread')
53 self.addCleanup(q_thread_patcher.stop)
54 q_thread_patcher.start()
55
56 def test_failed_download(self):
57 """
58 Test that icon get set to indicate a failure when `DownloadWorker` emits the download_failed signal
59 """
60 # GIVEN: An instance of `DownloadWorker`
61 instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue
62 worker_threads = Registry().get('application').worker_threads
63 worker = worker_threads['thumbnail_download_BlueBurst.png']['worker']
64
65 # WHEN: `DownloadWorker` emits the `download_failed` signal
66 worker.download_failed.emit()
67
68 # THEN: Then the initial loading icon should have been replaced by the exception icon
69 self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(UiIcons().exception)])
70
71 @patch('openlp.core.ui.firsttimeform.build_icon')
72 def test_successful_download(self, mocked_build_icon):
73 """
74 Test that the downloaded thumbnail is set as the icon when `DownloadWorker` emits the `download_succeeded`
75 signal
76 """
77 # GIVEN: An instance of `DownloadWorker`
78 instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue
79 worker_threads = Registry().get('application').worker_threads
80 worker = worker_threads['thumbnail_download_BlueBurst.png']['worker']
81 test_path = Path('downlaoded', 'file')
82
83 # WHEN: `DownloadWorker` emits the `download_succeeded` signal
84 worker.download_succeeded.emit(test_path)
85
86 # THEN: An icon should have been built from the downloaded file and used to replace the loading icon
87 mocked_build_icon.assert_called_once_with(test_path)
88 self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(mocked_build_icon())])