Merge branch 'ftw-remote-download' into 'master'

Web Remote Version Checking and Downloads; Other Fixes

See merge request openlp/openlp!159
This commit is contained in:
Tim Bentley 2020-04-08 07:00:39 +00:00
commit 92d67468e2
21 changed files with 631 additions and 110 deletions

View File

@ -23,16 +23,64 @@ Download and "install" the remote web client
""" """
import json import json
import logging import logging
from datetime import date
from distutils.version import LooseVersion
from zipfile import ZipFile from zipfile import ZipFile
from PyQt5 import QtCore
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import download_file, get_web_page, get_openlp_user_agent from openlp.core.common.httputils import download_file, get_web_page, get_openlp_user_agent
from openlp.core.common.registry import Registry
from openlp.core.threading import ThreadWorker, run_thread
REMOTE_URL = 'https://get.openlp.org/remote/' REMOTE_URL = 'https://get.openlp.org/remote/'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class RemoteVersionWorker(ThreadWorker):
"""
A worker class to fetch the version of the web remote. This is run from within a thread so that it
doesn't affect the loading time of OpenLP.
"""
new_version = QtCore.pyqtSignal(str)
no_internet = QtCore.pyqtSignal()
def __init__(self, current_version):
"""
Constructor for the version check worker.
:param string current_version: The current version of the web remote
"""
log.debug('VersionWorker - Initialise')
super().__init__(None)
self.current_version = current_version or '0.0'
def start(self):
"""
Check the latest version of the web remote against the version file on the OpenLP server.
"""
log.debug('RemoteVersionWorker - Start')
version_info = None
retries = 0
while retries < 3:
try:
version_info = download_version_info()
log.debug('New version found: %s', version_info['latest']['version'])
break
except OSError:
log.exception('Unable to connect to OpenLP server to download version file')
retries += 1
else:
self.no_internet.emit()
if version_info and LooseVersion(version_info['latest']['version']) > LooseVersion(self.current_version):
Registry().get('settings').setValue('api/last version test', date.today().strftime('%Y-%m-%d'))
Registry().get('settings_form').api_tab.master_version = version_info['latest']['version']
self.new_version.emit(version_info['latest']['version'])
self.quit.emit()
def deploy_zipfile(app_root_path, zip_name): def deploy_zipfile(app_root_path, zip_name):
""" """
Process the downloaded zip file and add to the correct directory Process the downloaded zip file and add to the correct directory
@ -60,7 +108,18 @@ def download_version_info():
return json.loads(file_contents) return json.loads(file_contents)
def download_and_check(callback=None): def get_latest_size():
"""
Download the version info file and get the size of the latest file
"""
version_info = download_version_info()
if not version_info:
log.warning('Unable to access the version information, abandoning download')
return 0
return version_info['latest']['size']
def download_and_check(callback=None, can_update_range=True):
""" """
Download the web site and deploy it. Download the web site and deploy it.
""" """
@ -70,9 +129,27 @@ def download_and_check(callback=None):
# Show the user an error message # Show the user an error message
return None return None
file_size = version_info['latest']['size'] file_size = version_info['latest']['size']
callback.setRange(0, file_size) if can_update_range:
callback.setRange(0, file_size)
if download_file(callback, REMOTE_URL + '{version}/{filename}'.format(**version_info['latest']), if download_file(callback, REMOTE_URL + '{version}/{filename}'.format(**version_info['latest']),
AppLocation.get_section_data_path('remotes') / 'remote.zip'): AppLocation.get_section_data_path('remotes') / 'remote.zip'):
deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'remote.zip') deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'remote.zip')
return version_info['latest']['version'] return version_info['latest']['version']
return None return None
def check_for_remote_update(main_window):
"""
Run a thread to download and check the version of OpenLP
:param MainWindow main_window: The OpenLP main window.
"""
last_check_date = Registry().get('settings').value('api/last version test')
if date.today().strftime('%Y-%m-%d') <= last_check_date:
log.debug('Version check skipped, last checked today')
return
worker = RemoteVersionWorker(Registry().get('settings').value('api/download version'))
worker.new_version.connect(main_window.on_new_remote_version)
# TODO: Use this to figure out if there's an Internet connection?
# worker.no_internet.connect(main_window.on_no_internet)
run_thread(worker, 'remote-version')

View File

@ -44,7 +44,7 @@ class ApiTab(SettingsTab):
def __init__(self, parent): def __init__(self, parent):
self.icon_path = UiIcons().remote self.icon_path = UiIcons().remote
advanced_translated = translate('OpenLP.APITab', 'API') advanced_translated = translate('OpenLP.APITab', 'API')
self.master_version = None self._master_version = None
super(ApiTab, self).__init__(parent, 'api', advanced_translated) super(ApiTab, self).__init__(parent, 'api', advanced_translated)
def setup_ui(self): def setup_ui(self):
@ -192,7 +192,37 @@ class ApiTab(SettingsTab):
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
self.current_version_label.setText(translate('RemotePlugin.RemoteTab', 'Current version:')) self.current_version_label.setText(translate('RemotePlugin.RemoteTab', 'Current version:'))
self.master_version_label.setText(translate('RemotePlugin.RemoteTab', 'Latest version:')) self.master_version_label.setText(translate('RemotePlugin.RemoteTab', 'Latest version:'))
self.unknown_version = translate('RemotePlugin.RemoteTab', '(unknown)') self._unknown_version = translate('RemotePlugin.RemoteTab', '(unknown)')
@property
def master_version(self):
"""
Property getter for the remote master version
"""
return self._master_version
@master_version.setter
def master_version(self, value):
"""
Property setter for the remote master version
"""
self._master_version = value
self.master_version_value.setText(self._master_version or self._unknown_version)
self.upgrade_button.setEnabled(self.can_enable_upgrade_button())
def can_enable_upgrade_button(self):
"""
Do a couple checks to set the upgrade button state
"""
return self.master_version_value.text() != self._unknown_version and \
self.master_version_value.text() != self.current_version_value.text()
def set_master_version(self):
"""
Check if the master version is not set, and set it to None to invoke the "unknown version" label
"""
if not self._master_version:
self.master_version = None
def set_urls(self): def set_urls(self):
""" """
@ -222,13 +252,6 @@ class ApiTab(SettingsTab):
break break
return ip_address return ip_address
def can_enable_upgrade_button(self):
"""
Do a couple checks to set the upgrade button state
"""
return self.master_version_value.text() != self.unknown_version and \
self.master_version_value.text() != self.current_version_value.text()
def load(self): def load(self):
""" """
Load the configuration and update the server configuration if necessary Load the configuration and update the server configuration if necessary
@ -243,8 +266,7 @@ class ApiTab(SettingsTab):
self.user_id.setText(self.settings.value(self.settings_section + '/user id')) self.user_id.setText(self.settings.value(self.settings_section + '/user id'))
self.password.setText(self.settings.value(self.settings_section + '/password')) self.password.setText(self.settings.value(self.settings_section + '/password'))
self.current_version_value.setText(self.settings.value(self.settings_section + '/download version')) self.current_version_value.setText(self.settings.value(self.settings_section + '/download version'))
self.master_version_value.setText(self.master_version or self.unknown_version) self.set_master_version()
self.upgrade_button.setEnabled(self.can_enable_upgrade_button())
self.set_urls() self.set_urls()
def save(self): def save(self):
@ -287,8 +309,7 @@ class ApiTab(SettingsTab):
app.process_events() app.process_events()
version_info = download_version_info() version_info = download_version_info()
app.process_events() app.process_events()
self.master_version_value.setText(version_info['latest']['version']) self.master_version = version_info['latest']['version']
self.upgrade_button.setEnabled(self.can_enable_upgrade_button())
app.process_events() app.process_events()
app.set_normal_cursor() app.set_normal_cursor()
app.process_events() app.process_events()

View File

@ -37,18 +37,19 @@ from traceback import format_exception
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets # noqa from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets # noqa
from openlp.core.state import State from openlp.core.api.deploy import check_for_remote_update
from openlp.core.common import is_macosx, is_win from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.mixins import LogMixin
from openlp.core.loader import loader
from openlp.core.common.i18n import LanguageManager, UiStrings, translate from openlp.core.common.i18n import LanguageManager, UiStrings, translate
from openlp.core.common.mixins import LogMixin
from openlp.core.common.path import create_paths from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.loader import loader
from openlp.core.resources import qInitResources from openlp.core.resources import qInitResources
from openlp.core.server import Server from openlp.core.server import Server
from openlp.core.state import State
from openlp.core.ui.exceptionform import ExceptionForm from openlp.core.ui.exceptionform import ExceptionForm
from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
@ -140,6 +141,8 @@ class OpenLP(QtCore.QObject, LogMixin):
self.main_window.first_time() self.main_window.first_time()
if self.settings.value('core/update check'): if self.settings.value('core/update check'):
check_for_update(self.main_window) check_for_update(self.main_window)
if self.settings.value('api/update check'):
check_for_remote_update(self.main_window)
self.main_window.is_display_blank() self.main_window.is_display_blank()
Registry().execute('bootstrap_completion') Registry().execute('bootstrap_completion')
return self.exec() return self.exec()

View File

@ -196,6 +196,8 @@ class Settings(QtCore.QSettings):
'api/ip address': '0.0.0.0', 'api/ip address': '0.0.0.0',
'api/thumbnails': True, 'api/thumbnails': True,
'api/download version': '0.0', 'api/download version': '0.0',
'api/last version test': '',
'api/update check': True,
'bibles/db type': 'sqlite', 'bibles/db type': 'sqlite',
'bibles/db username': '', 'bibles/db username': '',
'bibles/db password': '', 'bibles/db password': '',

View File

@ -32,6 +32,7 @@ from tempfile import gettempdir
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.api.deploy import get_latest_size, download_and_check
from openlp.core.common import trace_error_handler from openlp.core.common import trace_error_handler
from openlp.core.common.applocation import AppLocation from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import DownloadWorker, 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
@ -113,13 +114,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
""" """
Returns the id of the next FirstTimePage to go to based on enabled plugins Returns the id of the next FirstTimePage to go to based on enabled plugins
""" """
if FirstTimePage.Download < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked(): if FirstTimePage.Remote < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
# If the songs plugin is enabled then go to the songs page # If the songs plugin is enabled then go to the songs page
return FirstTimePage.Songs return FirstTimePage.Songs
elif FirstTimePage.Download < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked(): elif FirstTimePage.Remote < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
# Otherwise, if the Bibles plugin is enabled then go to the Bibles page # Otherwise, if the Bibles plugin is enabled then go to the Bibles page
return FirstTimePage.Bibles return FirstTimePage.Bibles
elif FirstTimePage.Download < self.currentId() < FirstTimePage.Themes: elif FirstTimePage.Remote < self.currentId() < FirstTimePage.Themes:
# Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes # Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes
return FirstTimePage.Themes return FirstTimePage.Themes
else: else:
@ -135,7 +136,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
if not self.has_web_access: if not self.has_web_access:
return FirstTimePage.NoInternet return FirstTimePage.NoInternet
else: else:
return FirstTimePage.Songs return FirstTimePage.Remote
elif self.currentId() == FirstTimePage.Progress: elif self.currentId() == FirstTimePage.Progress:
return -1 return -1
elif self.currentId() == FirstTimePage.NoInternet: elif self.currentId() == FirstTimePage.NoInternet:
@ -237,6 +238,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.has_run_wizard = self.settings.value('core/has run wizard') self.has_run_wizard = self.settings.value('core/has run wizard')
create_paths(Path(gettempdir(), 'openlp')) create_paths(Path(gettempdir(), 'openlp'))
self.theme_combo_box.clear() self.theme_combo_box.clear()
self.remote_page.can_download_remote = False
self.button(QtWidgets.QWizard.CustomButton1).setVisible(False) self.button(QtWidgets.QWizard.CustomButton1).setVisible(False)
if self.has_run_wizard: if self.has_run_wizard:
self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
@ -418,6 +420,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
for item in self.themes_list_widget.selectedItems(): for item in self.themes_list_widget.selectedItems():
size = get_url_file_size('{url}{file}'.format(url=self.themes_url, file=item.file_name)) size = get_url_file_size('{url}{file}'.format(url=self.themes_url, file=item.file_name))
self.max_progress += size self.max_progress += size
# If we're downloading the remote, add it in here too
if self.remote_page.can_download_remote:
self.max_progress += get_latest_size()
except urllib.error.URLError: except urllib.error.URLError:
trace_error_handler(log) trace_error_handler(log)
critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
@ -517,6 +522,15 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
if not download_file(self, '{url}{file}'.format(url=self.themes_url, file=item.file_name), if not download_file(self, '{url}{file}'.format(url=self.themes_url, file=item.file_name),
themes_destination_path / item.file_name, item.sha256): themes_destination_path / item.file_name, item.sha256):
missed_files.append('Theme: name'.format(name=item.file_name)) missed_files.append('Theme: name'.format(name=item.file_name))
# Remote
if self.remote_page.can_download_remote:
self._increment_progress_bar(self.downloading.format(name='Web Remote'), 0)
self.previous_size = 0
remote_version = download_and_check(self, can_update_range=False)
if remote_version:
self.settings.setValue('api/download version', remote_version)
else:
missed_files.append('Web Remote')
if missed_files: if missed_files:
file_list = '' file_list = ''
for entry in missed_files: for entry in missed_files:

View File

@ -29,6 +29,7 @@ from openlp.core.lib.ui import add_welcome_page
from openlp.core.ui.icons import UiIcons from openlp.core.ui.icons import UiIcons
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
from openlp.core.pages import GridLayoutPage
from openlp.core.widgets.widgets import ScreenSelectionWidget from openlp.core.widgets.widgets import ScreenSelectionWidget
@ -42,10 +43,53 @@ class FirstTimePage(object):
SampleOption = 3 SampleOption = 3
Download = 4 Download = 4
NoInternet = 5 NoInternet = 5
Songs = 6 Remote = 6
Bibles = 7 Songs = 7
Themes = 8 Bibles = 8
Progress = 9 Themes = 9
Progress = 10
class RemotePage(GridLayoutPage):
"""
A page for the web remote
"""
def setup_ui(self):
"""
Set up the page
"""
self.remote_label = QtWidgets.QLabel(self)
self.remote_label.setWordWrap(True)
self.remote_label.setObjectName('remote_label')
self.layout.addWidget(self.remote_label, 0, 0, 1, 4)
self.download_checkbox = QtWidgets.QCheckBox(self)
self.setObjectName('download_checkbox')
self.layout.addWidget(self.download_checkbox, 1, 1, 1, 3)
def retranslate_ui(self):
"""
Translate the interface
"""
self.remote_label.setText(translate('OpenLP.FirstTimeWizard', 'OpenLP has a web remote, which enables you to '
'control OpenLP from another computer, phone or tablet on the same network '
'as the OpenLP computer. OpenLP can download this web remote for you now, '
'or you can download it later via the remote settings.'))
self.download_checkbox.setText(translate('OpenLP.FirstTimeWizard', 'Yes, download the remote now'))
self.setTitle(translate('OpenLP.FirstTimeWizard', 'Web-based Remote Interface'))
self.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please confirm if you want to download the web remote.'))
@property
def can_download_remote(self):
"""
The get method of a property to determine if the user selected the "Download remote now" checkbox
"""
return self.download_checkbox.isChecked()
@can_download_remote.setter
def can_download_remote(self, value):
if not isinstance(value, bool):
raise TypeError('Must be a bool')
self.download_checkbox.setChecked(value)
class ThemeListWidget(QtWidgets.QListWidget): class ThemeListWidget(QtWidgets.QListWidget):
@ -187,6 +231,9 @@ class UiFirstTimeWizard(object):
self.alert_check_box.setObjectName('alert_check_box') self.alert_check_box.setObjectName('alert_check_box')
self.plugin_layout.addWidget(self.alert_check_box) self.plugin_layout.addWidget(self.alert_check_box)
first_time_wizard.setPage(FirstTimePage.Plugins, self.plugin_page) first_time_wizard.setPage(FirstTimePage.Plugins, self.plugin_page)
# Web Remote page
self.remote_page = RemotePage(self)
first_time_wizard.setPage(FirstTimePage.Remote, self.remote_page)
# The song samples page # The song samples page
self.songs_page = QtWidgets.QWizardPage() self.songs_page = QtWidgets.QWizardPage()
self.songs_page.setObjectName('songs_page') self.songs_page.setObjectName('songs_page')

View File

@ -622,8 +622,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
def on_new_version(self, version): def on_new_version(self, version):
""" """
Notifies the user that a newer version of OpenLP is available. Notifies the user that a newer version of OpenLP is available. Triggered by delay thread and cannot display
Triggered by delay thread and cannot display popup. popup.
:param version: The Version to be displayed. :param version: The Version to be displayed.
""" """
@ -632,6 +632,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
'https://openlp.org/.').format(new=version, current=get_version()[u'full']) 'https://openlp.org/.').format(new=version, current=get_version()[u'full'])
QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text) QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text)
def on_new_remote_version(self, version):
"""
Notifies the user that a newer version of the web remote is available. Triggered by delay thread and cannot
display popup.
:param version: The Version to be displayed.
"""
version_text = translate('OpenLP.MainWindow', 'Version {version} of the web remote is now available for '
'download.\nTo download this version, go to the Remote settings and click the Upgrade '
'button.').format(version=version)
self.information_message(translate('OpenLP.MainWindow', 'New Web Remote Version Available'), version_text)
def show(self): def show(self):
""" """
Show the main form, as well as the display form Show the main form, as well as the display form

View File

@ -61,7 +61,7 @@ class CustomPlugin(Plugin):
@staticmethod @staticmethod
def about(): def about():
about_text = translate('CustomPlugin', '<strong>Custom Slide Plugin </strong><br />The custom slide plugin ' about_text = translate('CustomPlugin', '<strong>Custom Slide Plugin</strong><br />The custom slide plugin '
'provides the ability to set up custom text slides that can be displayed on the screen ' 'provides the ability to set up custom text slides that can be displayed on the screen '
'the same way songs are. This plugin provides greater freedom over the songs plugin.') 'the same way songs are. This plugin provides greater freedom over the songs plugin.')
return about_text return about_text

View File

@ -70,7 +70,9 @@ class OpenLyricsExport(RegistryProperties):
xml = open_lyrics.song_to_xml(song) xml = open_lyrics.song_to_xml(song)
tree = etree.ElementTree(etree.fromstring(xml.encode())) tree = etree.ElementTree(etree.fromstring(xml.encode()))
filename = '{title} ({author})'.format(title=song.title, filename = '{title} ({author})'.format(title=song.title,
author=', '.join([author.display_name for author in song.authors])) author=', '.join([author.display_name for author in
sorted(song.authors,
key=lambda a: a.display_name)]))
filename = clean_filename(filename) filename = clean_filename(filename)
# Ensure the filename isn't too long for some filesystems # Ensure the filename isn't too long for some filesystems
path_length = len(str(self.save_path)) path_length = len(str(self.save_path))

View File

@ -187,7 +187,22 @@ class TestOpenLPJSONDecoder(TestCase):
""" """
# GIVEN: A JSON encoded string # GIVEN: A JSON encoded string
json_string = '[{"parts": ["test", "path1"], "json_meta": {"class": "Path", "version": 1}}, ' \ json_string = '[{"parts": ["test", "path1"], "json_meta": {"class": "Path", "version": 1}}, ' \
'{"parts": ["test", "path2"], "json_meta": {"class": "Path", "version": 1}}]' '{"parts": ["test", "path2"], "json_meta": {"class": "Path", "version": 1}}, ' \
'{"key": "value", "json_meta": {"class": "Object"}}]'
# WHEN: Decoding the string using the OpenLPJsonDecoder class
obj = json.loads(json_string, cls=OpenLPJSONDecoder)
# THEN: The object returned should be a python version of the JSON string
assert obj == [Path('test', 'path1'), Path('test', 'path2'), {'key': 'value', 'json_meta': {'class': 'Object'}}]
def test_json_decode_old_style(self):
"""
Test the OpenLPJsonDecoder when decoding a JSON string with an old-style Path object
"""
# GIVEN: A JSON encoded string
json_string = '[{"__Path__": ["test", "path1"]}, ' \
'{"__Path__": ["test", "path2"]}]'
# WHEN: Decoding the string using the OpenLPJsonDecoder class # WHEN: Decoding the string using the OpenLPJsonDecoder class
obj = json.loads(json_string, cls=OpenLPJSONDecoder) obj = json.loads(json_string, cls=OpenLPJSONDecoder)
@ -284,6 +299,19 @@ class TestPathSerializer(TestCase):
# THEN: A JSON decodeable object should have been returned. # THEN: A JSON decodeable object should have been returned.
assert obj == {'parts': (os.sep, 'base', 'path', 'to', 'fi.le'), "json_meta": {"class": "Path", "version": 1}} assert obj == {'parts': (os.sep, 'base', 'path', 'to', 'fi.le'), "json_meta": {"class": "Path", "version": 1}}
def test_path_json_object_is_js(self):
"""
Test that `Path.json_object` creates a JSON decode-able object from a Path object
"""
# GIVEN: A Path object from openlp.core.common.path
path = Path('/base', 'path', 'to', 'fi.le')
# WHEN: Calling json_object
obj = PathSerializer().json_object(path, is_js=True, extra=1, args=2)
# THEN: A URI should be returned
assert obj == 'file:///base/path/to/fi.le'
def test_path_json_object_base_path(self): def test_path_json_object_base_path(self):
""" """
Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the Test that `Path.json_object` creates a JSON decode-able object from a Path object, that is relative to the

View File

@ -29,7 +29,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem
from openlp.core.ui.firsttimewizard import ThemeListWidget from openlp.core.ui.firsttimewizard import RemotePage, ThemeListWidget
INVALID_CONFIG = """ INVALID_CONFIG = """
@ -367,6 +367,50 @@ def test_on_projectors_check_box_unchecked(mock_settings):
mock_settings.setValue.assert_called_once_with('projector/show after wizard', True) mock_settings.setValue.assert_called_once_with('projector/show after wizard', True)
def test_remote_page_get_can_download_remote(ftf_app):
"""
Test that the `can_download_remote` property returns the correct value
"""
# GIVEN: A RemotePage object with a mocked out download_checkbox
remote_page = RemotePage(None)
remote_page.download_checkbox = MagicMock(**{"isChecked.return_value": True})
# WHEN: The can_download_remote property is accessed
result = remote_page.can_download_remote
# THEN: The result should be True
assert result is True
def test_remote_page_set_can_download_remote(ftf_app):
"""
Test that the `can_download_remote` property sets the correct value
"""
# GIVEN: A RemotePage object with a mocked out download_checkbox
remote_page = RemotePage(None)
remote_page.download_checkbox = MagicMock()
# WHEN: The can_download_remote property is set
remote_page.can_download_remote = False
# THEN: The result should be True
remote_page.download_checkbox.setChecked.assert_called_once_with(False)
def test_remote_page_set_can_download_remote_not_bool(ftf_app):
"""
Test that the `can_download_remote` property throws an exception when the value is not a boolean
"""
# GIVEN: A RemotePage object with a mocked out download_checkbox
remote_page = RemotePage(None)
remote_page.download_checkbox = MagicMock()
# WHEN: The can_download_remote property is set
# THEN: An exception is thrown
with pytest.raises(TypeError, match='Must be a bool'):
remote_page.can_download_remote = 'not a bool'
def test_theme_list_widget_resize(ftf_app): def test_theme_list_widget_resize(ftf_app):
""" """
Test that the resizeEvent() method in the ThemeListWidget works correctly Test that the resizeEvent() method in the ThemeListWidget works correctly
@ -382,5 +426,3 @@ def test_theme_list_widget_resize(ftf_app):
# THEN: Check that the correct calculations were done # THEN: Check that the correct calculations were done
mocked_setGridSize.assert_called_once_with(QtCore.QSize(149, 140)) mocked_setGridSize.assert_called_once_with(QtCore.QSize(149, 140))
# THEN: everything resizes correctly

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Alerts plugin.
"""
from openlp.plugins.alerts.alertsplugin import AlertsPlugin
def test_plugin_about():
result = AlertsPlugin.about()
assert result == (
'<strong>Alerts Plugin</strong>'
'<br />The alert plugin controls the displaying of alerts on the display screen.'
)

View File

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Bibles plugin.
"""
from openlp.plugins.bibles.bibleplugin import BiblePlugin
def test_plugin_about():
result = BiblePlugin.about()
assert result == (
'<strong>Bible Plugin</strong>'
'<br />The Bible plugin provides the ability to display Bible '
'verses from different sources during the service.'
)

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Custom plugin.
"""
from openlp.plugins.custom.customplugin import CustomPlugin
def test_plugin_about():
result = CustomPlugin.about()
assert result == (
'<strong>Custom Slide Plugin</strong><br />The custom slide plugin '
'provides the ability to set up custom text slides that can be displayed on the screen '
'the same way songs are. This plugin provides greater freedom over the songs plugin.'
)

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Images plugin.
"""
from openlp.plugins.images.imageplugin import ImagePlugin
def test_image_plugin_about():
result = ImagePlugin.about()
assert result == (
'<strong>Image Plugin</strong>'
'<br />The image plugin provides displaying of images.<br />One '
'of the distinguishing features of this plugin is the ability to '
'group a number of images together in the service manager, making '
'the displaying of multiple images easier. This plugin can also '
'make use of OpenLP\'s "timed looping" feature to create a slide '
'show that runs automatically. In addition to this, images from '
'the plugin can be used to override the current theme\'s '
'background, which renders text-based items like songs with the '
'selected image as a background instead of the background '
'provided by the theme.'
)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Media plugin.
"""
from openlp.plugins.media.mediaplugin import MediaPlugin
def test_plugin_about():
result = MediaPlugin.about()
assert result == (
'<strong>Media Plugin</strong>'
'<br />The media plugin provides playback of audio and video.'
)

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Presentation plugin.
"""
from openlp.plugins.presentations.presentationplugin import PresentationPlugin
def test_plugin_about():
result = PresentationPlugin.about()
assert result == (
'<strong>Presentation '
'Plugin</strong><br />The presentation plugin provides the '
'ability to show presentations using a number of different '
'programs. The choice of available presentation programs is '
'available to the user in a drop down box.'
)

View File

@ -24,93 +24,108 @@ This module contains tests for the OpenLyrics song importer.
import shutil import shutil
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from openlp.core.common.registry import Registry import pytest
from openlp.core.common.settings import Settings
# from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport
from tests.helpers.testmixin import TestMixin
class TestOpenLyricsExport(TestCase, TestMixin): @pytest.yield_fixture
def temp_folder():
temp_path = Path(mkdtemp())
yield temp_path
shutil.rmtree(temp_path)
def test_export_same_filename(registry, settings, temp_folder):
""" """
Test the functions in the :mod:`openlyricsexport` module. Test that files is not overwritten if songs has same title and author
""" """
def setUp(self): # GIVEN: A mocked song_to_xml, 2 mocked songs, a mocked application and an OpenLyricsExport instance
""" with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml:
Create the registry mocked_song_to_xml.return_value = '<?xml version="1.0" encoding="UTF-8"?>\n<empty/>'
""" author = MagicMock()
Registry.create() author.display_name = 'Test Author'
Registry().register('settings', Settings()) song = MagicMock()
self.temp_folder = Path(mkdtemp()) song.authors = [author]
song.title = 'Test Title'
parent = MagicMock()
parent.stop_export_flag = False
# mocked_application_object = MagicMock()
# Registry().register('application', mocked_application_object)
ol_export = OpenLyricsExport(parent, [song, song], temp_folder)
def tearDown(self): # WHEN: Doing the export
""" ol_export.do_export()
Cleanup
"""
shutil.rmtree(self.temp_folder)
def test_export_same_filename(self): # THEN: The exporter should have created 2 files
""" assert (temp_folder / '{title} ({display_name}).xml'.format(
Test that files is not overwritten if songs has same title and author title=song.title, display_name=author.display_name)).exists() is True
""" assert (temp_folder / '{title} ({display_name})-1.xml'.format(
# GIVEN: A mocked song_to_xml, 2 mocked songs, a mocked application and an OpenLyricsExport instance title=song.title, display_name=author.display_name)).exists() is True
with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml:
mocked_song_to_xml.return_value = '<?xml version="1.0" encoding="UTF-8"?>\n<empty/>'
author = MagicMock()
author.display_name = 'Test Author'
song = MagicMock()
song.authors = [author]
song.title = 'Test Title'
parent = MagicMock()
parent.stop_export_flag = False
mocked_application_object = MagicMock()
Registry().register('application', mocked_application_object)
ol_export = OpenLyricsExport(parent, [song, song], self.temp_folder)
# WHEN: Doing the export
ol_export.do_export()
# THEN: The exporter should have created 2 files def test_export_sort_of_authers_filename(registry, settings, temp_folder):
assert (self.temp_folder / '{title} ({display_name}).xml'.format( """
title=song.title, display_name=author.display_name)).exists() is True Test that files is not overwritten if songs has same title and author
assert (self.temp_folder / '{title} ({display_name})-1.xml'.format( """
title=song.title, display_name=author.display_name)).exists() is True # GIVEN: A mocked song_to_xml, 1 mocked songs, a mocked application and an OpenLyricsExport instance
with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml:
mocked_song_to_xml.return_value = '<?xml version="1.0" encoding="UTF-8"?>\n<empty/>'
authorA = MagicMock()
authorA.display_name = 'a Author'
authorB = MagicMock()
authorB.display_name = 'b Author'
songA = MagicMock()
songA.authors = [authorA, authorB]
songA.title = 'Test Title'
songB = MagicMock()
songB.authors = [authorB, authorA]
songB.title = 'Test Title'
def test_export_sort_of_authers_filename(self): parent = MagicMock()
""" parent.stop_export_flag = False
Test that files is not overwritten if songs has same title and author # mocked_application_object = MagicMock()
""" # Registry().register('application', mocked_application_object)
# GIVEN: A mocked song_to_xml, 1 mocked songs, a mocked application and an OpenLyricsExport instance ol_export = OpenLyricsExport(parent, [songA, songB], temp_folder)
with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml:
mocked_song_to_xml.return_value = '<?xml version="1.0" encoding="UTF-8"?>\n<empty/>'
authorA = MagicMock()
authorA.display_name = 'a Author'
authorB = MagicMock()
authorB.display_name = 'b Author'
songA = MagicMock()
songA.authors = [authorA, authorB]
songA.title = 'Test Title'
songB = MagicMock()
songB.authors = [authorB, authorA]
songB.title = 'Test Title'
parent = MagicMock() # WHEN: Doing the export
parent.stop_export_flag = False ol_export.do_export()
mocked_application_object = MagicMock()
Registry().register('application', mocked_application_object)
ol_export = OpenLyricsExport(parent, [songA, songB], self.temp_folder)
# WHEN: Doing the export # THEN: The exporter orders authers
ol_export.do_export() assert (temp_folder / '{title} ({display_name}).xml'.format(
title=songA.title,
display_name=", ".join([authorA.display_name, authorB.display_name])
)).exists() is True
assert (temp_folder / '{title} ({display_name})-1.xml'.format(
title=songB.title,
display_name=", ".join([authorA.display_name, authorB.display_name])
)).exists() is True
# THEN: The exporter orders authers
assert (self.temp_folder / '{title} ({display_name}).xml'.format( def test_export_is_stopped(registry, settings, temp_folder):
title=song.title, """
display_name=", ".join([authorA.display_name, authorB.display_name]) Test that the exporter stops when the flag is set
)).exists() is True """
assert (self.temp_folder / '{title} ({display_name})-1.xml'.format( # GIVEN: A mocked song_to_xml, a mocked song, a mocked application and an OpenLyricsExport instance
title=song.title, with patch('openlp.plugins.songs.lib.openlyricsexport.OpenLyrics.song_to_xml') as mocked_song_to_xml:
display_name=", ".join([authorA.display_name, authorB.display_name]) mocked_song_to_xml.return_value = '<?xml version="1.0" encoding="UTF-8"?>\n<empty/>'
)).exists() is True author = MagicMock()
author.display_name = 'Test Author'
song = MagicMock()
song.authors = [author]
song.title = 'Test Title'
parent = MagicMock()
parent.stop_export_flag = True
# mocked_application_object = MagicMock()
# Registry().register('application', mocked_application_object)
ol_export = OpenLyricsExport(parent, [song, song], temp_folder)
# WHEN: Doing the export
ol_export.do_export()
# THEN: The exporter should not have created any files
assert (temp_folder / '{title} ({display_name}).xml'.format(
title=song.title, display_name=author.display_name)).exists() is False

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains tests for the plugin class Song plugin.
"""
from openlp.plugins.songs.songsplugin import SongsPlugin
def test_plugin_about():
result = SongsPlugin.about()
assert result == (
'<strong>Songs Plugin</strong>'
'<br />The songs plugin provides the ability to display and manage songs.'
)

View File

@ -22,7 +22,7 @@
Package to test the openlp.plugins.planningcenter.planningcenterplugin package. Package to test the openlp.plugins.planningcenter.planningcenterplugin package.
""" """
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch from unittest.mock import MagicMock, patch
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
@ -152,3 +152,16 @@ class TestPlanningCenterPlugin(TestCase, TestMixin):
return_value = self.plugin.about() return_value = self.plugin.about()
# THEN: # THEN:
self.assertGreater(len(return_value), 0, "About function returned some text") self.assertGreater(len(return_value), 0, "About function returned some text")
def test_finalise(self):
"""
Test that the finalise function cleans up after the plugin
"""
# GIVEN: A PlanningcenterPlugin Class with a bunch of mocks
self.plugin.import_planning_center = MagicMock()
# WHEN: finalise has been called on the class
self.plugin.finalise()
# THEN: it cleans up after itself
self.plugin.import_planning_center.setVisible.assert_called_once_with(False)