This commit is contained in:
Tomas Groth 2019-03-04 21:33:43 +01:00
commit d7e4798b96
6 changed files with 376 additions and 250 deletions

View File

@ -27,12 +27,16 @@ import logging
import sys
import time
from random import randint
from tempfile import gettempdir
import requests
from PyQt5 import QtCore
from openlp.core.common import trace_error_handler
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
from openlp.core.threading import ThreadWorker
log = logging.getLogger(__name__ + '.__init__')
@ -227,4 +231,46 @@ def download_file(update_object, url, file_path, sha256=None):
return True
__all__ = ['get_web_page']
class DownloadWorker(ThreadWorker):
"""
This worker allows a file to be downloaded in a thread
"""
download_failed = QtCore.pyqtSignal()
download_succeeded = QtCore.pyqtSignal(Path)
def __init__(self, base_url, file_name):
"""
Set up the worker object
"""
self._base_url = base_url
self._file_name = file_name
self._download_cancelled = False
super().__init__()
def start(self):
"""
Download the url to the temporary directory
"""
if self._download_cancelled:
self.quit.emit()
return
try:
dest_path = Path(gettempdir()) / 'openlp' / self._file_name
url = '{url}{name}'.format(url=self._base_url, name=self._file_name)
is_success = download_file(self, url, dest_path)
if is_success and not self._download_cancelled:
self.download_succeeded.emit(dest_path)
else:
self.download_failed.emit()
except Exception:
log.exception('Unable to download %s', url)
self.download_failed.emit()
finally:
self.quit.emit()
@QtCore.pyqtSlot()
def cancel_download(self):
"""
A slot to allow the download to be cancelled from outside of the thread
"""
self._download_cancelled = True

View File

@ -22,19 +22,19 @@
"""
This module contains the first time wizard.
"""
import json
import logging
import time
import urllib.error
import urllib.parse
import urllib.request
from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
from tempfile import gettempdir
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import clean_button_text, trace_error_handler
from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page
from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import Path, create_paths
@ -43,57 +43,50 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import build_icon
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.threading import ThreadWorker, get_thread_worker, is_thread_finished, run_thread
from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread
from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard
from openlp.core.ui.icons import UiIcons
log = logging.getLogger(__name__)
class ThemeScreenshotWorker(ThreadWorker):
class ThemeListWidgetItem(QtWidgets.QListWidgetItem):
"""
This thread downloads a theme's screenshot
Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource
"""
screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
def __init__(self, themes_url, sample_theme_data, ftw, *args, **kwargs):
super().__init__(*args, **kwargs)
title = sample_theme_data['title']
thumbnail = sample_theme_data['thumbnail']
self.file_name = sample_theme_data['file_name']
self.sha256 = sample_theme_data['sha256']
self.setIcon(UiIcons().picture) # Set a place holder icon whilst the thumbnails download
self.setText(title)
self.setToolTip(title)
worker = DownloadWorker(themes_url, thumbnail)
worker.download_failed.connect(self._on_download_failed)
worker.download_succeeded.connect(self._on_thumbnail_downloaded)
thread_name = 'thumbnail_download_{thumbnail}'.format(thumbnail=thumbnail)
run_thread(worker, thread_name)
ftw.thumbnail_download_threads.append(thread_name)
def __init__(self, themes_url, title, filename, sha256, screenshot):
def _on_download_failed(self):
"""
Set up the worker object
"""
self.was_cancelled = False
self.themes_url = themes_url
self.title = title
self.filename = filename
self.sha256 = sha256
self.screenshot = screenshot
super().__init__()
Set an icon to indicate that the thumbnail download has failed.
def start(self):
:rtype: None
"""
Run the worker
"""
if self.was_cancelled:
return
try:
download_path = Path(gettempdir()) / 'openlp' / self.screenshot
is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot),
download_path)
if is_success and not self.was_cancelled:
# Signal that the screenshot has been downloaded
self.screenshot_downloaded.emit(self.title, self.filename, self.sha256)
except: # noqa
log.exception('Unable to download screenshot')
finally:
self.quit.emit()
self.setIcon(UiIcons().exception)
@QtCore.pyqtSlot(bool)
def set_download_canceled(self, toggle):
def _on_thumbnail_downloaded(self, thumbnail_path):
"""
Externally set if the download was canceled
Load the thumbnail as the icon when it has downloaded.
:param toggle: Set if the download was canceled or not
:param Path thumbnail_path: Path to the file to use as a thumbnail
:rtype: None
"""
self.was_download_cancelled = toggle
self.setIcon(build_icon(thumbnail_path))
class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
@ -110,6 +103,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.web_access = True
self.web = ''
self.setup_ui(self)
self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed)
self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection)
self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll)
def get_next_page_id(self):
"""
@ -144,18 +140,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
return -1
elif self.currentId() == FirstTimePage.NoInternet:
return FirstTimePage.Progress
elif self.currentId() == FirstTimePage.Themes:
self.application.set_busy_cursor()
while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
time.sleep(0.1)
self.application.process_events()
# Build the screenshot icons, as this can not be done in the thread.
self._build_theme_screenshots()
self.application.set_normal_cursor()
self.theme_screenshot_threads = []
return self.get_next_page_id()
else:
return self.get_next_page_id()
return self.get_next_page_id()
def exec(self):
"""
@ -172,104 +157,76 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
"""
self.screens = screens
self.was_cancelled = False
self.theme_screenshot_threads = []
self.thumbnail_download_threads = []
self.has_run_wizard = False
self.themes_list_widget.itemChanged.connect(self.on_theme_selected)
def _download_index(self):
"""
Download the configuration file and kick off the theme screenshot download threads
"""
# check to see if we have web access
self.web_access = False
self.config = ConfigParser()
self.config = ''
web_config = None
user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
self.application.process_events()
try:
web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'),
web_config = get_web_page('{host}{name}'.format(host=self.web, name='download_3.0.json'),
headers={'User-Agent': user_agent})
except ConnectionError:
QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),
translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '
'to connect to retrieve initial configuration information'),
QtWidgets.QMessageBox.Ok)
web_config = False
if web_config:
try:
self.config.read_string(web_config)
self.web = self.config.get('general', 'base url')
self.songs_url = self.web + self.config.get('songs', 'directory') + '/'
self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/'
self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
self.web_access = True
except (NoSectionError, NoOptionError, MissingSectionHeaderError):
log.debug('A problem occurred while parsing the downloaded config file')
trace_error_handler(log)
if web_config and self._parse_config(web_config):
self.web_access = True
self.application.process_events()
self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
if self.has_run_wizard:
self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active())
self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
self.application.set_normal_cursor()
# Sort out internet access for downloads
if self.web_access:
songs = self.config.get('songs', 'languages')
songs = songs.split(',')
for song in songs:
def _parse_config(self, web_config):
try:
config = json.loads(web_config)
meta = config['_meta']
self.web = meta['base_url']
self.songs_url = self.web + meta['songs_dir'] + '/'
self.bibles_url = self.web + meta['bibles_dir'] + '/'
self.themes_url = self.web + meta['themes_dir'] + '/'
for song in config['songs'].values():
self.application.process_events()
title = self.config.get('songs_{song}'.format(song=song), 'title')
filename = self.config.get('songs_{song}'.format(song=song), 'filename')
sha256 = self.config.get('songs_{song}'.format(song=song), 'sha256', fallback='')
item = QtWidgets.QListWidgetItem(title, self.songs_list_widget)
item.setData(QtCore.Qt.UserRole, (filename, sha256))
item = QtWidgets.QListWidgetItem(song['title'], self.songs_list_widget)
item.setData(QtCore.Qt.UserRole, (song['file_name'], song['sha256']))
item.setCheckState(QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
bible_languages = self.config.get('bibles', 'languages')
bible_languages = bible_languages.split(',')
for lang in bible_languages:
for lang in config['bibles'].values():
self.application.process_events()
language = self.config.get('bibles_{lang}'.format(lang=lang), 'title')
lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [language])
bibles = self.config.get('bibles_{lang}'.format(lang=lang), 'translations')
bibles = bibles.split(',')
for bible in bibles:
lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [lang['title']])
for translation in lang['translations'].values():
self.application.process_events()
title = self.config.get('bible_{bible}'.format(bible=bible), 'title')
filename = self.config.get('bible_{bible}'.format(bible=bible), 'filename')
sha256 = self.config.get('bible_{bible}'.format(bible=bible), 'sha256', fallback='')
item = QtWidgets.QTreeWidgetItem(lang_item, [title])
item.setData(0, QtCore.Qt.UserRole, (filename, sha256))
item = QtWidgets.QTreeWidgetItem(lang_item, [translation['title']])
item.setData(0, QtCore.Qt.UserRole, (translation['file_name'], translation['sha256']))
item.setCheckState(0, QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
self.bibles_tree_widget.expandAll()
self.application.process_events()
# Download the theme screenshots
themes = self.config.get('themes', 'files').split(',')
for theme in themes:
title = self.config.get('theme_{theme}'.format(theme=theme), 'title')
filename = self.config.get('theme_{theme}'.format(theme=theme), 'filename')
sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='')
screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot)
worker.screenshot_downloaded.connect(self.on_screenshot_downloaded)
thread_name = 'theme_screenshot_{title}'.format(title=title)
run_thread(worker, thread_name)
self.theme_screenshot_threads.append(thread_name)
for theme in config['themes'].values():
ThemeListWidgetItem(self.themes_url, theme, self, self.themes_list_widget)
self.application.process_events()
except Exception:
log.exception('Unable to parse sample config file %s', web_config)
critical_error_message_box(
translate('OpenLP.FirstTimeWizard', 'Invalid index file'),
translate('OpenLP.FirstTimeWizard', 'OpenLP was unable to read the resource index file. '
'Please try again later.'))
return False
return True
def set_defaults(self):
"""
Set up display at start of theme edit.
"""
self.restart()
self.web = 'http://openlp.org/files/frw/'
self.web = 'https://get.openlp.org/ftw/'
self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked)
self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked)
@ -282,9 +239,18 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
create_paths(Path(gettempdir(), 'openlp'))
self.theme_combo_box.clear()
if self.has_run_wizard:
self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
self.presentation_check_box.setChecked(
self.plugin_manager.get_plugin_by_name('presentations').is_active())
self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
# Add any existing themes to list.
for theme in self.theme_manager.get_themes():
self.theme_combo_box.addItem(theme)
self.theme_combo_box.insertSeparator(0)
self.theme_combo_box.addItems(sorted(self.theme_manager.get_themes()))
default_theme = Settings().value('themes/global theme')
# Pre-select the current default theme.
index = self.theme_combo_box.findText(default_theme)
@ -335,49 +301,34 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
Process the triggering of the cancel button.
"""
self.was_cancelled = True
if self.theme_screenshot_threads:
for thread_name in self.theme_screenshot_threads:
if self.thumbnail_download_threads: # TODO: Use main thread list
for thread_name in self.thumbnail_download_threads:
worker = get_thread_worker(thread_name)
if worker:
worker.set_download_canceled(True)
worker.cancel_download()
# Was the thread created.
if self.theme_screenshot_threads:
while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]):
if self.thumbnail_download_threads:
while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]):
time.sleep(0.1)
self.application.set_normal_cursor()
def on_screenshot_downloaded(self, title, filename, sha256):
def on_themes_list_widget_selection_changed(self):
"""
Add an item to the list when a theme has been downloaded
Update the `theme_combo_box` with the selected items
:param title: The title of the theme
:param filename: The filename of the theme
"""
self.themes_list_widget.blockSignals(True)
item = QtWidgets.QListWidgetItem(title, self.themes_list_widget)
item.setData(QtCore.Qt.UserRole, (filename, sha256))
item.setCheckState(QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
self.themes_list_widget.blockSignals(False)
def on_theme_selected(self, item):
"""
Add or remove a de/selected sample theme from the theme_combo_box
:param QtWidgets.QListWidgetItem item: The item that has been de/selected
:rtype: None
"""
theme_name = item.text()
if self.theme_manager and theme_name in self.theme_manager.get_themes():
return True
if item.checkState() == QtCore.Qt.Checked:
self.theme_combo_box.addItem(theme_name)
return True
else:
index = self.theme_combo_box.findText(theme_name)
if index != -1:
self.theme_combo_box.removeItem(index)
return True
existing_themes = []
if self.theme_manager:
existing_themes = self.theme_manager.get_themes()
for list_index in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(list_index)
if item.text() not in existing_themes:
cbox_index = self.theme_combo_box.findText(item.text())
if item.isSelected() and cbox_index == -1:
self.theme_combo_box.insertItem(0, item.text())
elif not item.isSelected() and cbox_index != -1:
self.theme_combo_box.removeItem(cbox_index)
def on_no_internet_finish_button_clicked(self):
"""
@ -396,18 +347,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.was_cancelled = True
self.close()
def _build_theme_screenshots(self):
"""
This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``.
"""
themes = self.config.get('themes', 'files')
themes = themes.split(',')
for index, theme in enumerate(themes):
screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot')
item = self.themes_list_widget.item(index)
if item:
item.setIcon(build_icon(Path(gettempdir(), 'openlp', screenshot)))
def update_progress(self, count, block_size):
"""
Calculate and display the download progress. This method is called by download_file().
@ -456,13 +395,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.max_progress += size
iterator += 1
# Loop through the themes list and increase for each selected item
for i in range(self.themes_list_widget.count()):
self.application.process_events()
item = self.themes_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
filename, sha256 = item.data(QtCore.Qt.UserRole)
size = get_url_file_size('{path}{name}'.format(path=self.themes_url, name=filename))
self.max_progress += size
for item in self.themes_list_widget.selectedItems():
size = get_url_file_size('{url}{file}'.format(url=self.themes_url, file=item.file_name))
self.max_progress += size
except urllib.error.URLError:
trace_error_handler(log)
critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
@ -579,15 +514,12 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
missed_files.append('Bible: {name}'.format(name=bible))
bibles_iterator += 1
# Download themes
for i in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
theme, sha256 = item.data(QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=theme), 0)
self.previous_size = 0
if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
themes_destination_path / theme, sha256):
missed_files.append('Theme: {name}'.format(name=theme))
for item in self.themes_list_widget.selectedItems():
self._increment_progress_bar(self.downloading.format(name=item.file_name), 0)
self.previous_size = 0
if not download_file(self, '{url}{file}'.format(url=self.themes_url, file=item.file_name),
themes_destination_path / item.file_name, item.sha256):
missed_files.append('Theme: name'.format(name=item.file_name))
if missed_files:
file_list = ''
for entry in missed_files:

View File

@ -49,6 +49,39 @@ class FirstTimePage(object):
Progress = 8
class ThemeListWidget(QtWidgets.QListWidget):
"""
Subclass a QListWidget so we can make it look better when it resizes.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.setIconSize(QtCore.QSize(133, 100))
self.setMovement(QtWidgets.QListView.Static)
self.setFlow(QtWidgets.QListView.LeftToRight)
self.setProperty("isWrapping", True)
self.setResizeMode(QtWidgets.QListView.Adjust)
self.setViewMode(QtWidgets.QListView.IconMode)
self.setUniformItemSizes(True)
def resizeEvent(self, event):
"""
Resize the grid so the list looks better when its resized/
:param QtGui.QResizeEvent event: Not used
:return: None
"""
nominal_width = 141 # Icon width of 133 + 4 each side
max_items_per_row = self.viewport().width() // nominal_width or 1 # or 1 to avoid divide by 0 errors
col_size = (self.viewport().width() - 1) / max_items_per_row
self.setGridSize(QtCore.QSize(col_size, 140))
class UiFirstTimeWizard(object):
"""
The UI widgets for the first time wizard.
@ -175,27 +208,26 @@ class UiFirstTimeWizard(object):
self.themes_page = QtWidgets.QWizardPage()
self.themes_page.setObjectName('themes_page')
self.themes_layout = QtWidgets.QVBoxLayout(self.themes_page)
self.themes_layout.setContentsMargins(20, 50, 20, 60)
self.themes_layout.setObjectName('themes_layout')
self.themes_list_widget = QtWidgets.QListWidget(self.themes_page)
self.themes_list_widget.setViewMode(QtWidgets.QListView.IconMode)
self.themes_list_widget.setMovement(QtWidgets.QListView.Static)
self.themes_list_widget.setFlow(QtWidgets.QListView.LeftToRight)
self.themes_list_widget.setSpacing(4)
self.themes_list_widget.setUniformItemSizes(True)
self.themes_list_widget.setIconSize(QtCore.QSize(133, 100))
self.themes_list_widget.setWrapping(False)
self.themes_list_widget.setObjectName('themes_list_widget')
self.themes_list_widget = ThemeListWidget(self.themes_page)
self.themes_layout.addWidget(self.themes_list_widget)
self.theme_options_layout = QtWidgets.QHBoxLayout()
self.default_theme_layout = QtWidgets.QHBoxLayout()
self.theme_label = QtWidgets.QLabel(self.themes_page)
self.default_theme_layout.addWidget(self.theme_label)
self.theme_combo_box = QtWidgets.QComboBox(self.themes_page)
self.theme_combo_box.setEditable(False)
self.theme_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.theme_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
self.default_theme_layout.addWidget(self.theme_combo_box)
self.themes_layout.addLayout(self.default_theme_layout)
self.default_theme_layout.addWidget(self.theme_combo_box, stretch=1)
self.theme_options_layout.addLayout(self.default_theme_layout, stretch=1)
self.select_buttons_layout = QtWidgets.QHBoxLayout()
self.themes_select_all_button = QtWidgets.QToolButton(self.themes_page)
self.themes_select_all_button.setIcon(UiIcons().select_all)
self.select_buttons_layout.addWidget(self.themes_select_all_button, stretch=1, alignment=QtCore.Qt.AlignRight)
self.themes_deselect_all_button = QtWidgets.QToolButton(self.themes_page)
self.themes_deselect_all_button.setIcon(UiIcons().select_none)
self.select_buttons_layout.addWidget(self.themes_deselect_all_button)
self.theme_options_layout.addLayout(self.select_buttons_layout, stretch=1)
self.themes_layout.addLayout(self.theme_options_layout)
first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page)
# Progress page
self.progress_page = QtWidgets.QWizardPage()
@ -271,9 +303,12 @@ class UiFirstTimeWizard(object):
self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.'))
self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles'))
self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.'))
# Themes Page
self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes'))
self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.'))
self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Select default theme:'))
self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Default theme:'))
self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all'))
self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all'))
self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring'))
self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded '
'and OpenLP is configured.'))

View File

@ -138,6 +138,8 @@ class UiIcons(object):
'search_plus': {'icon': 'fa.search-plus'},
'search_ref': {'icon': 'fa.institution'},
'search_text': {'icon': 'op.search-text'},
'select_all': {'icon': 'fa.check-square-o'},
'select_none': {'icon': 'fa.square-o'},
'settings': {'icon': 'fa.cogs'},
'shortcuts': {'icon': 'fa.wrench'},
'song_usage': {'icon': 'fa.line-chart'},

View File

@ -25,40 +25,69 @@ Package to test the openlp.core.ui.firsttimeform package.
import os
import tempfile
from unittest import TestCase
from unittest.mock import MagicMock, call, patch
from unittest.mock import MagicMock, call, patch, DEFAULT
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem
from tests.helpers.testmixin import TestMixin
FAKE_CONFIG = """
[general]
base url = http://example.com/frw/
[songs]
directory = songs
[bibles]
directory = bibles
[themes]
directory = themes
INVALID_CONFIG = """
{
"_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json",
"_meta": {
}
"""
FAKE_BROKEN_CONFIG = """
[general]
base url = http://example.com/frw/
[songs]
directory = songs
[bibles]
directory = bibles
"""
FAKE_INVALID_CONFIG = """
<html>
<head><title>This is not a config file</title></head>
<body>Some text</body>
</html>
"""
class TestThemeListWidgetItem(TestCase):
"""
Test the :class:`ThemeListWidgetItem` class
"""
def setUp(self):
self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash',
'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'}
download_worker_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker')
self.addCleanup(download_worker_patcher.stop)
self.mocked_download_worker = download_worker_patcher.start()
run_thread_patcher = patch('openlp.core.ui.firsttimeform.run_thread')
self.addCleanup(run_thread_patcher.stop)
self.mocked_run_thread = run_thread_patcher.start()
def test_init_sample_data(self):
"""
Test that the theme data is loaded correctly in to a ThemeListWidgetItem object when instantiated
"""
# GIVEN: A sample theme dictanary object
# WHEN: Creating an instance of `ThemeListWidgetItem`
instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock())
# THEN: The data should have been set correctly
assert instance.file_name == 'BlueBurst.otz'
assert instance.sha256 == 'sha_256_hash'
assert instance.text() == 'Blue Burst'
assert instance.toolTip() == 'Blue Burst'
self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png')
def test_init_download_worker(self):
"""
Test that the `DownloadWorker` worker is set up correctly and that the thread is started.
"""
# GIVEN: A sample theme dictanary object
mocked_ftw = MagicMock(spec=FirstTimeForm)
mocked_ftw.thumbnail_download_threads = []
# WHEN: Creating an instance of `ThemeListWidgetItem`
instance = ThemeListWidgetItem('url', self.sample_theme_data, mocked_ftw)
# THEN: The `DownloadWorker` should have been set up with the appropriate data
self.mocked_download_worker.assert_called_once_with('url', 'BlueBurst.png')
self.mocked_download_worker.download_failed.connect.called_once_with(instance._on_download_failed())
self.mocked_download_worker.download_succeeded.connect.called_once_with(instance._on_thumbnail_downloaded)
self.mocked_run_thread.assert_called_once_with(
self.mocked_download_worker(), 'thumbnail_download_BlueBurst.png')
assert mocked_ftw.thumbnail_download_threads == ['thumbnail_download_BlueBurst.png']
class TestFirstTimeForm(TestCase, TestMixin):
@ -92,7 +121,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
assert expected_screens == frw.screens, 'The screens should be correct'
assert frw.web_access is True, 'The default value of self.web_access should be True'
assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'
assert [] == frw.theme_screenshot_threads, 'The list of threads should be empty'
assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty'
assert frw.has_run_wizard is False, 'has_run_wizard should be False'
def test_set_defaults(self):
@ -109,6 +138,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \
patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \
patch.object(Registry, 'register_function') as mocked_register_function, \
patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \
patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \
@ -122,7 +152,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
# THEN: The default values should have been set
mocked_restart.assert_called_once()
assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set'
assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
frw.on_no_internet_finish_button_clicked)
@ -134,6 +164,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
mocked_theme_combo_box.clear.assert_called_once()
mocked_theme_manager.assert_not_called()
mocked_songs_check_box.assert_not_called()
def test_set_defaults_rerun(self):
"""
@ -150,12 +181,17 @@ class TestFirstTimeForm(TestCase, TestMixin):
patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \
patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT,
image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT,
song_usage_check_box=DEFAULT, alert_check_box=DEFAULT), \
patch.object(Registry, 'register_function') as mocked_register_function, \
patch('openlp.core.ui.firsttimeform.Settings', return_value=mocked_settings), \
patch('openlp.core.ui.firsttimeform.gettempdir', return_value='temp') as mocked_gettempdir, \
patch('openlp.core.ui.firsttimeform.create_paths') as mocked_create_paths, \
patch.object(frw.application, 'set_normal_cursor'):
mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['a', 'b', 'c']})
mocked_plugin_manager = MagicMock()
mocked_theme_manager = MagicMock(**{'get_themes.return_value': ['b', 'a', 'c']})
Registry().register('plugin_manager', mocked_plugin_manager)
Registry().register('theme_manager', mocked_theme_manager)
# WHEN: The set_defaults() method is run
@ -163,7 +199,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
# THEN: The default values should have been set
mocked_restart.assert_called_once()
assert 'http://openlp.org/files/frw/' == frw.web, 'The default URL should be set'
assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
frw.on_no_internet_finish_button_clicked)
@ -173,9 +209,13 @@ class TestFirstTimeForm(TestCase, TestMixin):
mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')])
mocked_gettempdir.assert_called_once()
mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
mocked_theme_manager.assert_not_called()
mocked_theme_manager.get_themes.assert_called_once()
mocked_theme_combo_box.clear.assert_called_once()
mocked_theme_combo_box.addItem.assert_has_calls([call('a'), call('b'), call('c')])
mocked_plugin_manager.get_plugin_by_name.assert_has_calls(
[call('songs'), call('bibles'), call('presentations'), call('images'), call('media'), call('custom'),
call('songusage'), call('alerts')], any_order=True)
mocked_plugin_manager.get_plugin_by_name.assert_has_calls([call().is_active()] * 8, any_order=True)
mocked_theme_combo_box.addItems.assert_called_once_with(['a', 'b', 'c'])
mocked_theme_combo_box.findText.assert_called_once_with('Default Theme')
mocked_theme_combo_box.setCurrentIndex(3)
@ -192,7 +232,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
mocked_is_thread_finished.side_effect = [False, True]
frw = FirstTimeForm(None)
frw.initialize(MagicMock())
frw.theme_screenshot_threads = ['test_thread']
frw.thumbnail_download_threads = ['test_thread']
with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
# WHEN: on_cancel_button_clicked() is called
@ -201,43 +241,26 @@ class TestFirstTimeForm(TestCase, TestMixin):
# THEN: The right things should be called in the right order
assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'
mocked_get_thread_worker.assert_called_once_with('test_thread')
mocked_worker.set_download_canceled.assert_called_with(True)
mocked_worker.cancel_download.assert_called_once()
mocked_is_thread_finished.assert_called_with('test_thread')
assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
mocked_time.sleep.assert_called_once_with(0.1)
mocked_set_normal_cursor.assert_called_once_with()
def test_broken_config(self):
@patch('openlp.core.ui.firsttimeform.critical_error_message_box')
def test__parse_config_invalid_config(self, mocked_critical_error_message_box):
"""
Test if we can handle an config file with missing data
Test `FirstTimeForm._parse_config` when called with invalid data
"""
# GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked broken config file
with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:
first_time_form = FirstTimeForm(None)
first_time_form.initialize(MagicMock())
mocked_get_web_page.return_value = FAKE_BROKEN_CONFIG
# GIVEN: An instance of `FirstTimeForm`
first_time_form = FirstTimeForm(None)
# WHEN: The First Time Wizard is downloads the config file
first_time_form._download_index()
# WHEN: Calling _parse_config with a string containing invalid data
result = first_time_form._parse_config(INVALID_CONFIG)
# THEN: The First Time Form should not have web access
assert first_time_form.web_access is False, 'There should not be web access with a broken config file'
def test_invalid_config(self):
"""
Test if we can handle an config file in invalid format
"""
# GIVEN: A mocked get_web_page, a First Time Wizard, an expected screen object, and a mocked invalid config file
with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:
first_time_form = FirstTimeForm(None)
first_time_form.initialize(MagicMock())
mocked_get_web_page.return_value = FAKE_INVALID_CONFIG
# WHEN: The First Time Wizard is downloads the config file
first_time_form._download_index()
# THEN: The First Time Form should not have web access
assert first_time_form.web_access is False, 'There should not be web access with an invalid config file'
# THEN: _parse_data should return False and the user should have should have been informed.
assert result is False
mocked_critical_error_message_box.assert_called_once()
@patch('openlp.core.ui.firsttimeform.get_web_page')
@patch('openlp.core.ui.firsttimeform.QtWidgets.QMessageBox')

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Package to test the openlp.core.ui.firsttimeform package.
"""
from unittest import TestCase
from unittest.mock import MagicMock, call, patch
from openlp.core.common.path import Path
from openlp.core.common.registry import Registry
from openlp.core.ui.firsttimeform import ThemeListWidgetItem
from openlp.core.ui.icons import UiIcons
from tests.helpers.testmixin import TestMixin
class TestThemeListWidgetItem(TestCase, TestMixin):
def setUp(self):
self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash',
'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'}
Registry.create()
self.registry = Registry()
mocked_app = MagicMock()
mocked_app.worker_threads = {}
Registry().register('application', mocked_app)
self.setup_application()
move_to_thread_patcher = patch('openlp.core.ui.firsttimeform.DownloadWorker.moveToThread')
self.addCleanup(move_to_thread_patcher.stop)
move_to_thread_patcher.start()
set_icon_patcher = patch('openlp.core.ui.firsttimeform.ThemeListWidgetItem.setIcon')
self.addCleanup(set_icon_patcher.stop)
self.mocked_set_icon = set_icon_patcher.start()
q_thread_patcher = patch('openlp.core.ui.firsttimeform.QtCore.QThread')
self.addCleanup(q_thread_patcher.stop)
q_thread_patcher.start()
def test_failed_download(self):
"""
Test that icon get set to indicate a failure when `DownloadWorker` emits the download_failed signal
"""
# GIVEN: An instance of `DownloadWorker`
instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue
worker_threads = Registry().get('application').worker_threads
worker = worker_threads['thumbnail_download_BlueBurst.png']['worker']
# WHEN: `DownloadWorker` emits the `download_failed` signal
worker.download_failed.emit()
# THEN: Then the initial loading icon should have been replaced by the exception icon
self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(UiIcons().exception)])
@patch('openlp.core.ui.firsttimeform.build_icon')
def test_successful_download(self, mocked_build_icon):
"""
Test that the downloaded thumbnail is set as the icon when `DownloadWorker` emits the `download_succeeded`
signal
"""
# GIVEN: An instance of `DownloadWorker`
instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) # noqa Overcome GC issue
worker_threads = Registry().get('application').worker_threads
worker = worker_threads['thumbnail_download_BlueBurst.png']['worker']
test_path = Path('downlaoded', 'file')
# WHEN: `DownloadWorker` emits the `download_succeeded` signal
worker.download_succeeded.emit(test_path)
# THEN: An icon should have been built from the downloaded file and used to replace the loading icon
mocked_build_icon.assert_called_once_with(test_path)
self.mocked_set_icon.assert_has_calls([call(UiIcons().picture), call(mocked_build_icon())])