openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #33402
[Merge] lp:~phill-ridout/openlp/ftw-json-theme-list into lp:openlp
Phill has proposed merging lp:~phill-ridout/openlp/ftw-json-theme-list into lp:openlp.
Commit message:
move ftw to new json config format. spruce up theme list page
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/ftw-json-theme-list/+merge/363275
lp:~phill-ridout/openlp/ftw-json-theme-list (revision 2852)
https://ci.openlp.io/job/Branch-01-Pull/2681/ [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2575/ [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/345/ [SUCCESS]
https://ci.openlp.io/job/Branch-03a-Build-Source/186/ [SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/165/ [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Lint/1648/ [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1461/ [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/349/ [FAILURE]
--
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/ftw-json-theme-list into lp:openlp.
=== 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-15 23:05:12 +0000
@@ -27,12 +27,16 @@
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 @@
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 = f'{self._base_url}{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: # noqa
+ 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
=== 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-15 23:05:12 +0000
@@ -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.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):
- """
- This thread downloads a theme's screenshot
- """
- screenshot_downloaded = QtCore.pyqtSignal(str, str, str)
-
- def __init__(self, themes_url, title, filename, sha256, screenshot):
- """
- 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__()
-
- def start(self):
- """
- 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()
-
- @QtCore.pyqtSlot(bool)
- def set_download_canceled(self, toggle):
- """
- Externally set if the download was canceled
-
- :param toggle: Set if the download was canceled or not
- """
- self.was_download_cancelled = toggle
+class ThemeListWidgetItem(QtWidgets.QListWidgetItem):
+ """
+ Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource
+ """
+ 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 = f'thumbnail_download_{thumbnail}'
+ run_thread(worker, thread_name)
+ ftw.thumbnail_download_threads.append(thread_name)
+
+ def _on_download_failed(self):
+ """
+ Set an icon to indicate that the thumbnail download has failed.
+
+ :rtype: None
+ """
+ self.setIcon(UiIcons().exception)
+
+ def _on_thumbnail_downloaded(self, thumbnail_path):
+ """
+ Load the thumbnail as the icon when it has downloaded.
+
+ :param Path thumbnail_path: Path to the file to use as a thumbnail
+ :rtype: None
+ """
+ self.setIcon(build_icon(thumbnail_path))
class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
@@ -110,6 +103,9 @@
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 @@
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,83 @@
"""
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})
+ # web_config = Path(
+ # 'C:\\Users\\sroom\\Documents\\Phill Ridout\\play_ground\\openlp\\ftw-json\\download_3.0.json'
+ # ).read_text(encoding='utf-8') # TODO: Remove!!!!!
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 +246,18 @@
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 +308,34 @@
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):
- """
- Add an item to the list when a theme has been downloaded
-
- :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
+ def on_themes_list_widget_selection_changed(self):
+ """
+ Update the `theme_combo_box` with the selected items
+
: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 +354,6 @@
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 +402,9 @@
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(f'{self.themes_url}{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 +521,12 @@
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, f'{self.themes_url}{item.file_name}', themes_destination_path / item.file_name, item.sha256):
+ missed_files.append(f'Theme: {item.file_name}')
if missed_files:
file_list = ''
for entry in missed_files:
=== 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-15 23:05:12 +0000
@@ -49,6 +49,39 @@
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 @@
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 @@
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.'))
=== 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-15 23:05:12 +0000
@@ -138,6 +138,8 @@
'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'},
=== 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-15 23:05:12 +0000
@@ -25,40 +25,70 @@
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
-"""
-
-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>
-"""
+INVALID_CONFIG = """
+{
+ "_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json",
+ "_meta": {
+}
+"""
+
+
+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 +122,7 @@
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 +139,7 @@
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 +153,7 @@
# 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 +165,7 @@
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 +182,17 @@
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 +200,7 @@
# 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 +210,13 @@
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 +233,7 @@
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 +242,26 @@
# 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):
- """
- Test if we can handle an config file with missing 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
-
- # 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 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'
+ @patch('openlp.core.ui.firsttimeform.critical_error_message_box')
+ def test__parse_config_invalid_config(self, mocked_critical_error_message_box):
+ """
+ Test `FirstTimeForm._parse_config` when called with invalid data
+ """
+ # GIVEN: An instance of `FirstTimeForm`
+ first_time_form = FirstTimeForm(None)
+
+ # WHEN: Calling _parse_config with a string containing invalid data
+ result = first_time_form._parse_config(INVALID_CONFIG)
+
+ # 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')
=== 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-15 23:05:12 +0000
@@ -0,0 +1,89 @@
+# -*- 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())])
Follow ups