forked from openlp/openlp
head
This commit is contained in:
commit
f849b0eeb0
@ -7,6 +7,7 @@ cover
|
||||
.coverage
|
||||
coverage
|
||||
.directory
|
||||
.vscode
|
||||
dist
|
||||
*.dll
|
||||
documentation/build/doctrees
|
||||
|
@ -39,8 +39,8 @@ def deploy_zipfile(app_root_path, zip_name):
|
||||
:return: None
|
||||
"""
|
||||
zip_path = app_root_path / zip_name
|
||||
web_zip = ZipFile(str(zip_path))
|
||||
web_zip.extractall(str(app_root_path))
|
||||
web_zip = ZipFile(zip_path)
|
||||
web_zip.extractall(app_root_path)
|
||||
|
||||
|
||||
def download_sha256():
|
||||
|
@ -317,8 +317,7 @@ def set_up_logging(log_path):
|
||||
"""
|
||||
create_paths(log_path, do_not_log=True)
|
||||
file_path = log_path / 'openlp.log'
|
||||
# TODO: FileHandler accepts a Path object in Py3.6
|
||||
logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
|
||||
logfile = logging.FileHandler(file_path, 'w', encoding='UTF-8')
|
||||
logfile.setFormatter(logging.Formatter('%(asctime)s %(threadName)s %(name)-55s %(levelname)-8s %(message)s'))
|
||||
log.addHandler(logfile)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
@ -364,7 +363,7 @@ def main(args=None):
|
||||
portable_settings_path = data_path / 'OpenLP.ini'
|
||||
# Make this our settings file
|
||||
log.info('INI file: {name}'.format(name=portable_settings_path))
|
||||
Settings.set_filename(str(portable_settings_path))
|
||||
Settings.set_filename(portable_settings_path)
|
||||
portable_settings = Settings()
|
||||
# Set our data path
|
||||
log.info('Data path: {name}'.format(name=data_path))
|
||||
@ -405,7 +404,12 @@ def main(args=None):
|
||||
None, translate('OpenLP', 'Settings Upgrade'),
|
||||
translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at '
|
||||
'{back_up_path}').format(back_up_path=back_up_path))
|
||||
settings.export(back_up_path)
|
||||
try:
|
||||
settings.export(back_up_path)
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
None, translate('OpenLP', 'Settings Upgrade'),
|
||||
translate('OpenLP', 'Settings back up failed.\n\nContinuining to upgrade.'))
|
||||
settings.upgrade_settings()
|
||||
# First time checks in settings
|
||||
if not Settings().value('core/has run wizard'):
|
||||
|
@ -474,10 +474,10 @@ def get_file_encoding(file_path):
|
||||
if not chunk:
|
||||
break
|
||||
detector.feed(chunk)
|
||||
detector.close()
|
||||
return detector.result
|
||||
except OSError:
|
||||
log.exception('Error detecting file encoding')
|
||||
finally:
|
||||
return detector.close()
|
||||
|
||||
|
||||
def normalize_str(irregular_string):
|
||||
|
@ -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
|
||||
|
@ -78,7 +78,7 @@ class Path(PathVariant):
|
||||
:param onerror: Handler function to handle any errors
|
||||
:rtype: None
|
||||
"""
|
||||
shutil.rmtree(str(self), ignore_errors, onerror)
|
||||
shutil.rmtree(self, ignore_errors, onerror)
|
||||
|
||||
|
||||
def replace_params(args, kwargs, params):
|
||||
|
@ -542,7 +542,7 @@ class Settings(QtCore.QSettings):
|
||||
old_values = [self._convert_value(old_value, default_value)
|
||||
for old_value, default_value in zip(old_values, default_values)]
|
||||
# Iterate over our rules and check what the old_value should be "converted" to.
|
||||
new_value = None
|
||||
new_value = old_values[0]
|
||||
for new_rule, old_rule in rules:
|
||||
# If the value matches with the condition (rule), then use the provided value. This is used to
|
||||
# convert values. E. g. an old value 1 results in True, and 0 in False.
|
||||
|
@ -6,7 +6,7 @@
|
||||
<style type="text/css">
|
||||
body {
|
||||
background: transparent !important;
|
||||
color: #fff !important;
|
||||
color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
sup {
|
||||
vertical-align: super !important;
|
||||
|
@ -315,7 +315,7 @@ var Display = {
|
||||
section.setAttribute("style", "height: 100%; width: 100%; position: relative;");
|
||||
var img = document.createElement('img');
|
||||
img.src = image;
|
||||
img.setAttribute("style", "position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;");
|
||||
img.setAttribute("style", "position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; max-height: 100%; max-width: 100%");
|
||||
section.appendChild(img);
|
||||
slidesDiv.appendChild(section);
|
||||
Display._slides['0'] = 0;
|
||||
|
@ -706,6 +706,7 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
|
||||
else:
|
||||
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.
|
||||
raw_list = raw_list[index + 1:]
|
||||
log.debug(raw_list)
|
||||
raw_list[0] = raw_tags + raw_list[0]
|
||||
html_list = html_list[index + 1:]
|
||||
html_list[0] = html_tags + html_list[0]
|
||||
|
@ -29,16 +29,14 @@ import copy
|
||||
|
||||
from PyQt5 import QtCore, QtWebChannel, QtWidgets
|
||||
|
||||
from openlp.core.common.path import Path, path_to_str
|
||||
from openlp.core.common.path import path_to_str
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.ui import HideMode
|
||||
from openlp.core.display.screens import ScreenList
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
DISPLAY_PATH = Path(__file__).parent / 'html' / 'display.html'
|
||||
CHECKERBOARD_PATH = Path(__file__).parent / 'html' / 'checkerboard.png'
|
||||
OPENLP_SPLASH_SCREEN_PATH = Path(__file__).parent / 'html' / 'openlp-splash-screen.png'
|
||||
|
||||
|
||||
class MediaWatcher(QtCore.QObject):
|
||||
@ -126,7 +124,11 @@ class DisplayWindow(QtWidgets.QWidget):
|
||||
self.webview.page().setBackgroundColor(QtCore.Qt.transparent)
|
||||
self.layout.addWidget(self.webview)
|
||||
self.webview.loadFinished.connect(self.after_loaded)
|
||||
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(DISPLAY_PATH)))
|
||||
display_base_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'display' / 'html'
|
||||
self.display_path = display_base_path / 'display.html'
|
||||
self.checkerboard_path = display_base_path / 'checkerboard.png'
|
||||
self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png'
|
||||
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
|
||||
self.media_watcher = MediaWatcher(self)
|
||||
self.channel = QtWebChannel.QWebChannel(self)
|
||||
self.channel.registerObject('mediaWatcher', self.media_watcher)
|
||||
@ -169,7 +171,7 @@ class DisplayWindow(QtWidgets.QWidget):
|
||||
bg_color = Settings().value('core/logo background color')
|
||||
image = Settings().value('core/logo file')
|
||||
if path_to_str(image).startswith(':'):
|
||||
image = OPENLP_SPLASH_SCREEN_PATH
|
||||
image = self.openlp_splash_screen_path
|
||||
image_uri = image.as_uri()
|
||||
self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color,
|
||||
image=image_uri))
|
||||
@ -329,7 +331,7 @@ class DisplayWindow(QtWidgets.QWidget):
|
||||
if theme.background_type == 'transparent' and not self.is_display:
|
||||
theme_copy = copy.deepcopy(theme)
|
||||
theme_copy.background_type = 'image'
|
||||
theme_copy.background_filename = CHECKERBOARD_PATH
|
||||
theme_copy.background_filename = self.checkerboard_path
|
||||
exported_theme = theme_copy.export_theme()
|
||||
else:
|
||||
exported_theme = theme.export_theme()
|
||||
|
@ -242,16 +242,16 @@ def image_to_byte(image, base_64=True):
|
||||
"""
|
||||
Resize an image to fit on the current screen for the web and returns it as a byte stream.
|
||||
|
||||
:param image: The image to converted.
|
||||
:param image: The image to be converted.
|
||||
:param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array.
|
||||
To preserve original intention, this defaults to True
|
||||
"""
|
||||
log.debug('image_to_byte - start')
|
||||
byte_array = QtCore.QByteArray()
|
||||
# use buffer to store pixmap into byteArray
|
||||
buffie = QtCore.QBuffer(byte_array)
|
||||
buffie.open(QtCore.QIODevice.WriteOnly)
|
||||
image.save(buffie, "PNG")
|
||||
buffer = QtCore.QBuffer(byte_array)
|
||||
buffer.open(QtCore.QIODevice.WriteOnly)
|
||||
image.save(buffer, "PNG")
|
||||
log.debug('image_to_byte - end')
|
||||
if not base_64:
|
||||
return byte_array
|
||||
@ -263,15 +263,13 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
||||
"""
|
||||
Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
|
||||
|
||||
:param image_path: The image file to create the icon from.
|
||||
:param thumb_path: The filename to save the thumbnail to.
|
||||
:param openlp.core.common.path.Path image_path: The image file to create the icon from.
|
||||
:param openlp.core.common.path.Path thumb_path: The filename to save the thumbnail to.
|
||||
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
||||
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
|
||||
height of 88 is used.
|
||||
:return: The final icon.
|
||||
"""
|
||||
# TODO: To path object
|
||||
thumb_path = Path(thumb_path)
|
||||
reader = QtGui.QImageReader(str(image_path))
|
||||
if size is None:
|
||||
# No size given; use default height of 88
|
||||
|
@ -132,8 +132,9 @@ def get_db_path(plugin_name, db_file_name=None):
|
||||
Create a path to a database from the plugin name and database name
|
||||
|
||||
:param plugin_name: Name of plugin
|
||||
:param db_file_name: File name of database
|
||||
:return: The path to the database as type str
|
||||
:param openlp.core.common.path.Path | str | None db_file_name: File name of database
|
||||
:return: The path to the database
|
||||
:rtype: str
|
||||
"""
|
||||
if db_file_name is None:
|
||||
return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name),
|
||||
@ -144,15 +145,15 @@ def get_db_path(plugin_name, db_file_name=None):
|
||||
return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), name=db_file_name)
|
||||
|
||||
|
||||
def handle_db_error(plugin_name, db_file_name):
|
||||
def handle_db_error(plugin_name, db_file_path):
|
||||
"""
|
||||
Log and report to the user that a database cannot be loaded
|
||||
|
||||
:param plugin_name: Name of plugin
|
||||
:param db_file_name: File name of database
|
||||
:param openlp.core.common.path.Path db_file_path: File name of database
|
||||
:return: None
|
||||
"""
|
||||
db_path = get_db_path(plugin_name, db_file_name)
|
||||
db_path = get_db_path(plugin_name, db_file_path)
|
||||
log.exception('Error loading database: {db}'.format(db=db_path))
|
||||
critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
|
||||
translate('OpenLP.Manager',
|
||||
@ -161,10 +162,13 @@ def handle_db_error(plugin_name, db_file_name):
|
||||
|
||||
def init_url(plugin_name, db_file_name=None):
|
||||
"""
|
||||
Return the database URL.
|
||||
Construct the connection string for a database.
|
||||
|
||||
:param plugin_name: The name of the plugin for the database creation.
|
||||
:param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
|
||||
:param openlp.core.common.path.Path | str | None db_file_name: The database file name. Defaults to None resulting
|
||||
in the plugin_name being used.
|
||||
:return: The database URL
|
||||
:rtype: str
|
||||
"""
|
||||
settings = Settings()
|
||||
settings.beginGroup(plugin_name)
|
||||
@ -345,7 +349,7 @@ class Manager(object):
|
||||
Runs the initialisation process that includes creating the connection to the database and the tables if they do
|
||||
not exist.
|
||||
|
||||
:param plugin_name: The name to setup paths and settings section names
|
||||
:param plugin_name: The name to setup paths and settings section names
|
||||
:param init_schema: The init_schema function for this database
|
||||
:param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None
|
||||
resulting in the plugin_name being used.
|
||||
@ -357,14 +361,14 @@ class Manager(object):
|
||||
self.db_url = None
|
||||
if db_file_path:
|
||||
log.debug('Manager: Creating new DB url')
|
||||
self.db_url = init_url(plugin_name, str(db_file_path))
|
||||
self.db_url = init_url(plugin_name, str(db_file_path)) # TOdO :PATHLIB
|
||||
else:
|
||||
self.db_url = init_url(plugin_name)
|
||||
if upgrade_mod:
|
||||
try:
|
||||
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
|
||||
except (SQLAlchemyError, DBAPIError):
|
||||
handle_db_error(plugin_name, str(db_file_path))
|
||||
handle_db_error(plugin_name, db_file_path)
|
||||
return
|
||||
if db_ver > up_ver:
|
||||
critical_error_message_box(
|
||||
@ -379,7 +383,7 @@ class Manager(object):
|
||||
try:
|
||||
self.session = init_schema(self.db_url)
|
||||
except (SQLAlchemyError, DBAPIError):
|
||||
handle_db_error(plugin_name, str(db_file_path))
|
||||
handle_db_error(plugin_name, db_file_path)
|
||||
else:
|
||||
self.session = session
|
||||
|
||||
|
@ -454,15 +454,16 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
"""
|
||||
pass
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Live):
|
||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Live,
|
||||
file_path=None):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service Item to be processed
|
||||
:param item: The database item to be used to build the service item
|
||||
:param xml_version:
|
||||
:param remote: Was this remote triggered (False)
|
||||
:param context: The service context
|
||||
:param openlp.core.common.path.Path file_path:
|
||||
"""
|
||||
raise NotImplementedError('MediaManagerItem.generate_slide_data needs to be defined by the plugin')
|
||||
|
||||
@ -624,17 +625,16 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
'You must select a {title} '
|
||||
'service item.').format(title=self.title))
|
||||
|
||||
def build_service_item(self, item=None, xml_version=False, remote=False, context=ServiceItemContext.Live):
|
||||
def build_service_item(self, item=None, remote=False, context=ServiceItemContext.Live):
|
||||
"""
|
||||
Common method for generating a service item
|
||||
:param item: Service Item to be built.
|
||||
:param xml_version: version of XML (False)
|
||||
:param remote: Remote triggered (False)
|
||||
:param context: The context on which this is called
|
||||
"""
|
||||
service_item = ServiceItem(self.plugin)
|
||||
service_item.add_icon()
|
||||
if self.generate_slide_data(service_item, item, xml_version, remote, context):
|
||||
if self.generate_slide_data(service_item, item=item, remote=remote, context=context):
|
||||
return service_item
|
||||
else:
|
||||
return None
|
||||
|
@ -263,7 +263,7 @@ class ServiceItem(RegistryProperties):
|
||||
file_location = os.path.join(path, file_name)
|
||||
file_location_hash = md5_hash(file_location.encode('utf-8'))
|
||||
image = os.path.join(str(AppLocation.get_section_data_path(self.name)), 'thumbnails',
|
||||
file_location_hash, ntpath.basename(image))
|
||||
file_location_hash, ntpath.basename(image)) # TODO: Pathlib
|
||||
self.slides.append({'title': file_name, 'image': image, 'path': path, 'display_title': display_title,
|
||||
'notes': notes, 'thumbnail': image})
|
||||
# if self.is_capable(ItemCapabilities.HasThumbnails):
|
||||
@ -361,7 +361,7 @@ class ServiceItem(RegistryProperties):
|
||||
if isinstance(file_path, str):
|
||||
# Handle service files prior to OpenLP 3.0
|
||||
# Windows can handle both forward and backward slashes, so we use ntpath to get the basename
|
||||
file_path = Path(path, ntpath.basename(file_path))
|
||||
file_path = path / ntpath.basename(file_path)
|
||||
self.background_audio.append(file_path)
|
||||
self.theme_overwritten = header.get('theme_overwritten', False)
|
||||
if self.service_item_type == ServiceItemType.Text:
|
||||
@ -374,7 +374,7 @@ class ServiceItem(RegistryProperties):
|
||||
if path:
|
||||
self.has_original_files = False
|
||||
for text_image in service_item['serviceitem']['data']:
|
||||
file_path = os.path.join(path, text_image)
|
||||
file_path = path / text_image
|
||||
self.add_from_image(file_path, text_image, background)
|
||||
else:
|
||||
for text_image in service_item['serviceitem']['data']:
|
||||
|
@ -417,11 +417,17 @@ class ProjectorDB(Manager):
|
||||
value: (str) From ProjectorSource, Sources tables or PJLink default code list
|
||||
"""
|
||||
source_dict = {}
|
||||
# Apparently, there was a change to the projector object. Test for which object has db id
|
||||
if hasattr(projector, "id"):
|
||||
chk = projector.id
|
||||
elif hasattr(projector.entry, "id"):
|
||||
chk = projector.entry.id
|
||||
|
||||
# Get default list first
|
||||
for key in projector.source_available:
|
||||
item = self.get_object_filtered(ProjectorSource,
|
||||
and_(ProjectorSource.code == key,
|
||||
ProjectorSource.projector_id == projector.id))
|
||||
ProjectorSource.projector_id == chk))
|
||||
if item is None:
|
||||
source_dict[key] = PJLINK_DEFAULT_CODES[key]
|
||||
else:
|
||||
|
@ -46,31 +46,10 @@ from openlp.core.projectors.sourceselectform import SourceSelectSingle, SourceSe
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.widgets.toolbar import OpenLPToolbar
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.debug('projectormanager loaded')
|
||||
|
||||
|
||||
# Dict for matching projector status to display icon
|
||||
STATUS_ICONS = {
|
||||
S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png',
|
||||
S_CONNECTING: ':/projector/projector_item_connect.png',
|
||||
S_CONNECTED: ':/projector/projector_off.png',
|
||||
S_OFF: ':/projector/projector_off.png',
|
||||
S_INITIALIZE: ':/projector/projector_off.png',
|
||||
S_STANDBY: ':/projector/projector_off.png',
|
||||
S_WARMUP: ':/projector/projector_warmup.png',
|
||||
S_ON: ':/projector/projector_on.png',
|
||||
S_COOLDOWN: ':/projector/projector_cooldown.png',
|
||||
E_ERROR: ':/projector/projector_error.png',
|
||||
E_NETWORK: ':/projector/projector_not_connected_error.png',
|
||||
E_SOCKET_TIMEOUT: ':/projector/projector_not_connected_error.png',
|
||||
E_AUTHENTICATION: ':/projector/projector_not_connected_error.png',
|
||||
E_UNKNOWN_SOCKET_ERROR: ':/projector/projector_not_connected_error.png',
|
||||
E_NOT_CONNECTED: ':/projector/projector_not_connected_error.png'
|
||||
}
|
||||
|
||||
|
||||
class UiProjectorManager(object):
|
||||
"""
|
||||
UI part of the Projector Manager
|
||||
@ -122,7 +101,7 @@ class UiProjectorManager(object):
|
||||
self.one_toolbar.add_toolbar_action('connect_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projector.'),
|
||||
icon=UiIcons().projector_connect,
|
||||
icon=UiIcons().projector_select_connect,
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projector.'),
|
||||
triggers=self.on_connect_projector)
|
||||
@ -136,7 +115,7 @@ class UiProjectorManager(object):
|
||||
self.one_toolbar.add_toolbar_action('disconnect_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projectors'),
|
||||
icon=UiIcons().projector_disconnect,
|
||||
icon=UiIcons().projector_select_disconnect,
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projector.'),
|
||||
triggers=self.on_disconnect_projector)
|
||||
@ -151,7 +130,7 @@ class UiProjectorManager(object):
|
||||
self.one_toolbar.add_toolbar_action('poweron_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Power on selected projector'),
|
||||
icon=UiIcons().projector_on,
|
||||
icon=UiIcons().projector_power_on,
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Power on selected projector.'),
|
||||
triggers=self.on_poweron_projector)
|
||||
@ -164,7 +143,7 @@ class UiProjectorManager(object):
|
||||
triggers=self.on_poweron_projector)
|
||||
self.one_toolbar.add_toolbar_action('poweroff_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
|
||||
icon=UiIcons().projector_off,
|
||||
icon=UiIcons().projector_power_off,
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Put selected projector in standby.'),
|
||||
triggers=self.on_poweroff_projector)
|
||||
@ -309,7 +288,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
|
||||
S_INITIALIZE: UiIcons().projector_on,
|
||||
S_STANDBY: UiIcons().projector_off,
|
||||
S_WARMUP: UiIcons().projector_warmup,
|
||||
S_ON: UiIcons().projector_off,
|
||||
S_ON: UiIcons().projector_on,
|
||||
S_COOLDOWN: UiIcons().projector_cooldown,
|
||||
E_ERROR: UiIcons().projector_error,
|
||||
E_NETWORK: UiIcons().error,
|
||||
@ -880,6 +859,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
|
||||
"""
|
||||
Update the icons when the selected projectors change
|
||||
"""
|
||||
log.debug('update_icons(): Checking for selected projector items in list')
|
||||
count = len(self.projector_list_widget.selectedItems())
|
||||
projector = None
|
||||
if count == 0:
|
||||
@ -900,6 +880,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
|
||||
self.get_toolbar_item('blank_projector_multiple', hidden=True)
|
||||
self.get_toolbar_item('show_projector_multiple', hidden=True)
|
||||
elif count == 1:
|
||||
log.debug('update_icons(): Found one item selected')
|
||||
projector = self.projector_list_widget.selectedItems()[0].data(QtCore.Qt.UserRole)
|
||||
connected = QSOCKET_STATE[projector.link.state()] == S_CONNECTED
|
||||
power = projector.link.power == S_ON
|
||||
@ -910,12 +891,14 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
|
||||
self.get_toolbar_item('blank_projector_multiple', hidden=True)
|
||||
self.get_toolbar_item('show_projector_multiple', hidden=True)
|
||||
if connected:
|
||||
log.debug('update_icons(): Updating icons for connected state')
|
||||
self.get_toolbar_item('view_projector', enabled=True)
|
||||
self.get_toolbar_item('source_view_projector',
|
||||
enabled=connected and power and projector.link.source_available is not None)
|
||||
enabled=projector.link.source_available is not None and connected and power)
|
||||
self.get_toolbar_item('edit_projector', hidden=True)
|
||||
self.get_toolbar_item('delete_projector', hidden=True)
|
||||
else:
|
||||
log.debug('update_icons(): Updating for not connected state')
|
||||
self.get_toolbar_item('view_projector', hidden=True)
|
||||
self.get_toolbar_item('source_view_projector', hidden=True)
|
||||
self.get_toolbar_item('edit_projector', enabled=True)
|
||||
@ -931,6 +914,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
|
||||
self.get_toolbar_item('blank_projector', enabled=False)
|
||||
self.get_toolbar_item('show_projector', enabled=False)
|
||||
else:
|
||||
log.debug('update_icons(): Updating for multiple items selected')
|
||||
self.get_toolbar_item('edit_projector', enabled=False)
|
||||
self.get_toolbar_item('delete_projector', enabled=False)
|
||||
self.get_toolbar_item('view_projector', hidden=True)
|
||||
|
@ -528,8 +528,9 @@ class PJLinkCommands(object):
|
||||
sources.append(source)
|
||||
sources.sort()
|
||||
self.source_available = sources
|
||||
log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.entry.name,
|
||||
data=self.source_available))
|
||||
log.debug('({ip}) Setting projector source_available to "{data}"'.format(ip=self.entry.name,
|
||||
data=self.source_available))
|
||||
self.projectorUpdateIcons.emit()
|
||||
return
|
||||
|
||||
def process_lamp(self, data):
|
||||
@ -886,6 +887,9 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
|
||||
elif status >= S_NOT_CONNECTED and status in QSOCKET_STATE:
|
||||
# Socket connection status update
|
||||
self.status_connect = status
|
||||
# Check if we need to update error state as well
|
||||
if self.error_status != S_OK and status != S_NOT_CONNECTED:
|
||||
self.error_status = S_OK
|
||||
elif status >= S_NOT_CONNECTED and status in PROJECTOR_STATE:
|
||||
# Only affects the projector status
|
||||
self.projector_status = status
|
||||
@ -905,7 +909,14 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
|
||||
message=status_message if msg is None else msg))
|
||||
|
||||
# Now that we logged extra information for debugging, broadcast the original change/message
|
||||
(code, message) = self._get_status(status)
|
||||
# Check for connection errors first
|
||||
if self.error_status != S_OK:
|
||||
log.debug('({ip}) Signalling error code'.format(ip=self.entry.name))
|
||||
code, message = self._get_status(self.error_status)
|
||||
status = self.error_status
|
||||
else:
|
||||
log.debug('({ip}) Signalling status code'.format(ip=self.entry.name))
|
||||
code, message = self._get_status(status)
|
||||
if msg is not None:
|
||||
message = msg
|
||||
elif message is None:
|
||||
@ -953,7 +964,7 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
|
||||
log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.entry.name))
|
||||
return self.disconnect_from_host()
|
||||
# Convert the initial login prompt with the expected PJLink normal command format for processing
|
||||
log.debug('({ip}) check_login(): Formatting initial connection prompt'
|
||||
log.debug('({ip}) check_login(): Formatting initial connection prompt '
|
||||
'to PJLink packet'.format(ip=self.entry.name))
|
||||
return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX,
|
||||
clss='1',
|
||||
|
@ -455,7 +455,7 @@ class AdvancedTab(SettingsTab):
|
||||
Service Name options changed
|
||||
"""
|
||||
self.service_name_day.setEnabled(default_service_enabled)
|
||||
time_enabled = default_service_enabled and self.service_name_day.currentIndex() is not 7
|
||||
time_enabled = default_service_enabled and self.service_name_day.currentIndex() != 7
|
||||
self.service_name_time.setEnabled(time_enabled)
|
||||
self.service_name_edit.setEnabled(default_service_enabled)
|
||||
self.service_name_revert_button.setEnabled(default_service_enabled)
|
||||
@ -478,7 +478,7 @@ class AdvancedTab(SettingsTab):
|
||||
minute=self.service_name_time.time().minute()
|
||||
)
|
||||
try:
|
||||
service_name_example = format_time(str(self.service_name_edit.text()), local_time)
|
||||
service_name_example = format_time(self.service_name_edit.text(), local_time)
|
||||
except ValueError:
|
||||
preset_is_valid = False
|
||||
service_name_example = translate('OpenLP.AdvancedTab', 'Syntax error.')
|
||||
@ -497,7 +497,7 @@ class AdvancedTab(SettingsTab):
|
||||
"""
|
||||
React to the day of the service name changing.
|
||||
"""
|
||||
self.service_name_time.setEnabled(service_day is not 7)
|
||||
self.service_name_time.setEnabled(service_day != 7)
|
||||
self.update_service_name_example(None)
|
||||
|
||||
def on_service_name_revert_button_clicked(self):
|
||||
|
@ -77,11 +77,11 @@ class Ui_ExceptionDialog(object):
|
||||
self.save_report_button = create_button(exception_dialog, 'save_report_button',
|
||||
icon=UiIcons().save,
|
||||
click=self.on_save_report_button_clicked)
|
||||
self.attach_tile_button = create_button(exception_dialog, 'attach_tile_button',
|
||||
self.attach_file_button = create_button(exception_dialog, 'attach_file_button',
|
||||
icon=UiIcons().open,
|
||||
click=self.on_attach_file_button_clicked)
|
||||
self.button_box = create_button_box(exception_dialog, 'button_box', ['close'],
|
||||
[self.send_report_button, self.save_report_button, self.attach_tile_button])
|
||||
[self.send_report_button, self.save_report_button, self.attach_file_button])
|
||||
self.exception_layout.addWidget(self.button_box)
|
||||
|
||||
self.retranslate_ui(exception_dialog)
|
||||
@ -112,4 +112,4 @@ class Ui_ExceptionDialog(object):
|
||||
).format(first_part=exception_part1))
|
||||
self.send_report_button.setText(translate('OpenLP.ExceptionDialog', 'Send E-Mail'))
|
||||
self.save_report_button.setText(translate('OpenLP.ExceptionDialog', 'Save to File'))
|
||||
self.attach_tile_button.setText(translate('OpenLP.ExceptionDialog', 'Attach File'))
|
||||
self.attach_file_button.setText(translate('OpenLP.ExceptionDialog', 'Attach File'))
|
||||
|
@ -95,12 +95,14 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
||||
"""
|
||||
Saving exception log and system information to a file.
|
||||
"""
|
||||
file_path, filter_used = FileDialog.getSaveFileName(
|
||||
self,
|
||||
translate('OpenLP.ExceptionForm', 'Save Crash Report'),
|
||||
Settings().value(self.settings_section + '/last directory'),
|
||||
translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
|
||||
if file_path:
|
||||
while True:
|
||||
file_path, filter_used = FileDialog.getSaveFileName(
|
||||
self,
|
||||
translate('OpenLP.ExceptionForm', 'Save Crash Report'),
|
||||
Settings().value(self.settings_section + '/last directory'),
|
||||
translate('OpenLP.ExceptionForm', 'Text files (*.txt *.log *.text)'))
|
||||
if file_path is None:
|
||||
break
|
||||
Settings().setValue(self.settings_section + '/last directory', file_path.parent)
|
||||
opts = self._create_report()
|
||||
report_text = self.report_text.format(version=opts['version'], description=opts['description'],
|
||||
@ -108,8 +110,13 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
||||
try:
|
||||
with file_path.open('w') as report_file:
|
||||
report_file.write(report_text)
|
||||
except OSError:
|
||||
break
|
||||
except OSError as e:
|
||||
log.exception('Failed to write crash report')
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, translate('OpenLP.ExceptionDialog', 'Failed to Save Report'),
|
||||
translate('OpenLP.ExceptionDialog', 'The following error occured when saving the report.\n\n'
|
||||
'{exception}').format(file_name=file_path, exception=e))
|
||||
|
||||
def on_send_report_button_clicked(self):
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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.'))
|
||||
|
@ -47,7 +47,6 @@ class GeneralTab(SettingsTab):
|
||||
"""
|
||||
Initialise the general settings tab
|
||||
"""
|
||||
self.logo_file = ':/graphics/openlp-splash-screen.png'
|
||||
self.logo_background_color = '#ffffff'
|
||||
self.screens = ScreenList()
|
||||
self.icon_path = ':/icon/openlp-logo.svg'
|
||||
|
@ -117,13 +117,17 @@ class UiIcons(object):
|
||||
'presentation': {'icon': 'fa.bar-chart'},
|
||||
'preview': {'icon': 'fa.laptop'},
|
||||
'projector': {'icon': 'op.video'},
|
||||
'projector_connect': {'icon': 'fa.plug'},
|
||||
'projector_connect': {'icon': 'fa.plug'}, # Projector connect
|
||||
'projector_cooldown': {'icon': 'fa.video-camera', 'attr': 'blue'},
|
||||
'projector_disconnect': {'icon': 'fa.plug', 'attr': 'lightGray'},
|
||||
'projector_disconnect': {'icon': 'fa.plug', 'attr': 'lightGray'}, # Projector disconnect
|
||||
'projector_error': {'icon': 'fa.video-camera', 'attr': 'red'},
|
||||
'projector_hdmi': {'icon': 'op.hdmi'},
|
||||
'projector_off': {'icon': 'fa.video-camera', 'attr': 'black'},
|
||||
'projector_on': {'icon': 'fa.video-camera', 'attr': 'green'},
|
||||
'projector_power_off': {'icon': 'fa.video-camera', 'attr': 'red'}, # Toolbar power off
|
||||
'projector_power_on': {'icon': 'fa.video-camera', 'attr': 'green'}, # Toolbar power on
|
||||
'projector_off': {'icon': 'fa.video-camera', 'attr': 'black'}, # Projector off
|
||||
'projector_on': {'icon': 'fa.video-camera', 'attr': 'green'}, # Projector on
|
||||
'projector_select_connect': {'icon': 'fa.plug', 'attr': 'green'}, # Toolbar connect
|
||||
'projector_select_disconnect': {'icon': 'fa.plug', 'attr': 'red'}, # Toolbar disconnect
|
||||
'projector_warmup': {'icon': 'fa.video-camera', 'attr': 'yellow'},
|
||||
'picture': {'icon': 'fa.picture-o'},
|
||||
'print': {'icon': 'fa.print'},
|
||||
@ -138,6 +142,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'},
|
||||
|
@ -1334,7 +1334,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
||||
self.show_status_message(
|
||||
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
|
||||
'- Please wait for copy to finish').format(path=self.new_data_path))
|
||||
dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
|
||||
dir_util.copy_tree(old_data_path, self.new_data_path)
|
||||
self.log_info('Copy successful')
|
||||
except (OSError, DistutilsFileError) as why:
|
||||
self.application.set_normal_cursor()
|
||||
|
@ -25,7 +25,6 @@ The service manager sets up, loads, saves and manages services.
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
@ -234,7 +233,7 @@ class Ui_ServiceManager(object):
|
||||
self.service_manager_list.itemExpanded.connect(self.expanded)
|
||||
# Last little bits of setting up
|
||||
self.service_theme = Settings().value(self.main_window.service_manager_settings_section + '/service theme')
|
||||
self.service_path = str(AppLocation.get_section_data_path('servicemanager'))
|
||||
self.service_path = AppLocation.get_section_data_path('servicemanager')
|
||||
# build the drag and drop context menu
|
||||
self.dnd_menu = QtWidgets.QMenu()
|
||||
self.new_action = self.dnd_menu.addAction(translate('OpenLP.ServiceManager', '&Add New Item'))
|
||||
@ -590,11 +589,11 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
self.main_window.increment_progress_bar(service_content_size)
|
||||
# Finally add all the listed media files.
|
||||
for write_path in write_list:
|
||||
zip_file.write(str(write_path), str(write_path))
|
||||
zip_file.write(write_path, write_path)
|
||||
self.main_window.increment_progress_bar(write_path.stat().st_size)
|
||||
with suppress(FileNotFoundError):
|
||||
file_path.unlink()
|
||||
os.link(temp_file.name, str(file_path))
|
||||
os.link(temp_file.name, file_path)
|
||||
Settings().setValue(self.main_window.service_manager_settings_section + '/last directory', file_path.parent)
|
||||
except (PermissionError, OSError) as error:
|
||||
self.log_exception('Failed to save service to disk: {name}'.format(name=file_path))
|
||||
@ -679,7 +678,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
service_data = None
|
||||
self.application.set_busy_cursor()
|
||||
try:
|
||||
with zipfile.ZipFile(str(file_path)) as zip_file:
|
||||
with zipfile.ZipFile(file_path) as zip_file:
|
||||
compressed_size = 0
|
||||
for zip_info in zip_file.infolist():
|
||||
compressed_size += zip_info.compress_size
|
||||
@ -692,7 +691,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
service_data = json_file.read()
|
||||
else:
|
||||
zip_info.filename = os.path.basename(zip_info.filename)
|
||||
zip_file.extract(zip_info, str(self.service_path))
|
||||
zip_file.extract(zip_info, self.service_path)
|
||||
self.main_window.increment_progress_bar(zip_info.compress_size)
|
||||
if service_data:
|
||||
items = json.loads(service_data, cls=OpenLPJsonDecoder)
|
||||
@ -705,11 +704,13 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
else:
|
||||
raise ValidationError(msg='No service data found')
|
||||
except (NameError, OSError, ValidationError, zipfile.BadZipFile):
|
||||
self.application.set_normal_cursor()
|
||||
self.log_exception('Problem loading service file {name}'.format(name=file_path))
|
||||
critical_error_message_box(
|
||||
message=translate('OpenLP.ServiceManager',
|
||||
'The service file {file_path} could not be loaded because it is either corrupt, or '
|
||||
'not a valid OpenLP 2 or OpenLP 3 service file.'.format(file_path=file_path)))
|
||||
'The service file {file_path} could not be loaded because it is either corrupt, '
|
||||
'inaccessible, or not a valid OpenLP 2 or OpenLP 3 service file.'
|
||||
).format(file_path=file_path))
|
||||
self.main_window.finished_progress_bar()
|
||||
self.application.set_normal_cursor()
|
||||
self.repaint_service_list(-1, -1)
|
||||
@ -1237,11 +1238,11 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
Empties the service_path of temporary files on system exit.
|
||||
"""
|
||||
for file_name in os.listdir(self.service_path):
|
||||
file_path = Path(self.service_path, file_name)
|
||||
for file_path in self.service_path.iterdir():
|
||||
delete_file(file_path)
|
||||
if os.path.exists(os.path.join(self.service_path, 'audio')):
|
||||
shutil.rmtree(os.path.join(self.service_path, 'audio'), True)
|
||||
audio_path = self.service_path / 'audio'
|
||||
if audio_path.exists():
|
||||
audio_path.rmtree(True)
|
||||
|
||||
def on_theme_combo_box_selected(self, current_index):
|
||||
"""
|
||||
|
@ -150,7 +150,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
self.global_theme = Settings().value(self.settings_section + '/global theme')
|
||||
self.build_theme_path()
|
||||
self.load_first_time_themes()
|
||||
self.upgrade_themes()
|
||||
self.upgrade_themes() # TODO: Can be removed when upgrade path from OpenLP 2.4 no longer needed
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
"""
|
||||
@ -422,10 +422,10 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(str(theme_path), 'w') as theme_zip:
|
||||
with zipfile.ZipFile(theme_path, 'w') as theme_zip:
|
||||
source_path = self.theme_path / theme_name
|
||||
for file_path in source_path.iterdir():
|
||||
theme_zip.write(str(file_path), os.path.join(theme_name, file_path.name))
|
||||
theme_zip.write(file_path, Path(theme_name, file_path.name))
|
||||
return True
|
||||
except OSError as ose:
|
||||
self.log_exception('Export Theme Failed')
|
||||
@ -567,10 +567,10 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
json_theme = False
|
||||
theme_name = ""
|
||||
try:
|
||||
with zipfile.ZipFile(str(file_path)) as theme_zip:
|
||||
with zipfile.ZipFile(file_path) as theme_zip:
|
||||
json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
|
||||
if len(json_file) != 1:
|
||||
# TODO: remove XML handling after the 2.6 release.
|
||||
# TODO: remove XML handling after once the upgrade path from 2.4 is no longer required
|
||||
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
|
||||
if len(xml_file) != 1:
|
||||
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
|
||||
@ -607,12 +607,12 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
else:
|
||||
with full_name.open('wb') as out_file:
|
||||
out_file.write(theme_zip.read(zipped_file))
|
||||
except (OSError, zipfile.BadZipFile):
|
||||
except (OSError, ValidationError, zipfile.BadZipFile):
|
||||
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
|
||||
raise ValidationError
|
||||
except ValidationError:
|
||||
critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
|
||||
translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
|
||||
critical_error_message_box(
|
||||
translate('OpenLP.ThemeManager', 'Import Error'),
|
||||
translate('OpenLP.ThemeManager', 'There was a problem imoorting {file_name}.\n\nIt is corrupt,'
|
||||
'inaccessible or not a valid theme.').format(file_name=file_path))
|
||||
finally:
|
||||
if not abort_import:
|
||||
# As all files are closed, we can create the Theme.
|
||||
|
@ -206,7 +206,7 @@ class ThemesTab(SettingsTab):
|
||||
find_and_set_in_combo_box(self.default_combo_box, self.global_theme)
|
||||
# self.renderer.set_global_theme()
|
||||
self.renderer.set_theme_level(self.theme_level)
|
||||
if self.global_theme is not '':
|
||||
if self.global_theme != '':
|
||||
self._preview_global_theme()
|
||||
|
||||
def _preview_global_theme(self):
|
||||
|
@ -200,7 +200,7 @@ def get_library_versions():
|
||||
"""
|
||||
library_versions = OrderedDict([(library, _get_lib_version(*args)) for library, args in LIBRARIES.items()])
|
||||
# Just update the dict for display purposes, changing the None to a '-'
|
||||
for library, version in library_versions:
|
||||
for library, version in library_versions.items():
|
||||
if version is None:
|
||||
library_versions[library] = '-'
|
||||
return library_versions
|
||||
|
@ -352,7 +352,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
:rtype: None
|
||||
"""
|
||||
if self._path != path:
|
||||
self._path = path
|
||||
self.path = path
|
||||
self.pathChanged.emit(path)
|
||||
|
||||
|
||||
|
@ -27,13 +27,11 @@ import logging
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import is_macosx
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib.ui import add_welcome_page
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.widgets.dialogs import FileDialog
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -280,41 +278,3 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
|
||||
self.finish_button.setVisible(True)
|
||||
self.cancel_button.setVisible(False)
|
||||
self.application.process_events()
|
||||
|
||||
def get_file_name(self, title, editbox, setting_name, filters=''):
|
||||
"""
|
||||
Opens a FileDialog and saves the filename to the given editbox.
|
||||
|
||||
:param str title: The title of the dialog.
|
||||
:param QtWidgets.QLineEdit editbox: An QLineEdit.
|
||||
:param str setting_name: The place where to save the last opened directory.
|
||||
:param str filters: The file extension filters. It should contain the file description
|
||||
as well as the file extension. For example::
|
||||
|
||||
'OpenLP 2 Databases (*.sqlite)'
|
||||
:rtype: None
|
||||
"""
|
||||
if filters:
|
||||
filters += ';;'
|
||||
filters += '%s (*)' % UiStrings().AllFiles
|
||||
file_path, filter_used = FileDialog.getOpenFileName(
|
||||
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), filters)
|
||||
if file_path:
|
||||
editbox.setText(str(file_path))
|
||||
Settings().setValue(self.plugin.settings_section + '/' + setting_name, file_path.parent)
|
||||
|
||||
def get_folder(self, title, editbox, setting_name):
|
||||
"""
|
||||
Opens a FileDialog and saves the selected folder to the given editbox.
|
||||
|
||||
:param str title: The title of the dialog.
|
||||
:param QtWidgets.QLineEdit editbox: An QLineEditbox.
|
||||
:param str setting_name: The place where to save the last opened directory.
|
||||
:rtype: None
|
||||
"""
|
||||
folder_path = FileDialog.getExistingDirectory(
|
||||
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
|
||||
FileDialog.ShowDirsOnly)
|
||||
if folder_path:
|
||||
editbox.setText(str(folder_path))
|
||||
Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)
|
||||
|
@ -463,14 +463,14 @@ class BibleImportForm(OpenLPWizard):
|
||||
UiStrings().NFSs, translate('BiblesPlugin.ImportWizardForm',
|
||||
'You need to specify a file with books of the Bible to use in the '
|
||||
'import.'))
|
||||
self.csv_books_edit.setFocus()
|
||||
self.csv_books_path_edit.setFocus()
|
||||
return False
|
||||
elif not self.field('csv_versefile'):
|
||||
critical_error_message_box(
|
||||
UiStrings().NFSs,
|
||||
translate('BiblesPlugin.ImportWizardForm', 'You need to specify a file of Bible verses to '
|
||||
'import.'))
|
||||
self.csv_verses_edit.setFocus()
|
||||
self.csv_verses_pathedit.setFocus()
|
||||
return False
|
||||
elif self.field('source_format') == BibleFormat.OpenSong:
|
||||
if not self.field('opensong_file'):
|
||||
|
@ -48,9 +48,9 @@ class BibleImport(BibleDB, LogMixin, RegistryProperties):
|
||||
"""
|
||||
Check if the supplied file is compressed
|
||||
|
||||
:param file_path: A path to the file to check
|
||||
:param openlp.core.common.path.Path file_path: A path to the file to check
|
||||
"""
|
||||
if is_zipfile(str(file_path)):
|
||||
if is_zipfile(file_path):
|
||||
critical_error_message_box(
|
||||
message=translate('BiblesPlugin.BibleImport',
|
||||
'The file "{file}" you supplied is compressed. You must decompress it before import.'
|
||||
|
@ -158,11 +158,10 @@ class BibleDB(Manager):
|
||||
self.name = kwargs['name']
|
||||
if not isinstance(self.name, str):
|
||||
self.name = str(self.name, 'utf-8')
|
||||
# TODO: To path object
|
||||
file_path = Path(clean_filename(self.name) + '.sqlite')
|
||||
self.file_path = Path(clean_filename(self.name) + '.sqlite')
|
||||
if 'file' in kwargs:
|
||||
file_path = kwargs['file']
|
||||
Manager.__init__(self, 'bibles', init_schema, file_path, upgrade)
|
||||
self.file_path = kwargs['file']
|
||||
Manager.__init__(self, 'bibles', init_schema, self.file_path, upgrade)
|
||||
if self.session and 'file' in kwargs:
|
||||
self.get_name()
|
||||
self._is_web_bible = None
|
||||
@ -750,7 +749,7 @@ class BiblesResourcesDB(QtCore.QObject, Manager):
|
||||
]
|
||||
|
||||
|
||||
class AlternativeBookNamesDB(QtCore.QObject, Manager):
|
||||
class AlternativeBookNamesDB(Manager):
|
||||
"""
|
||||
This class represents a database-bound alternative book names system.
|
||||
"""
|
||||
@ -765,8 +764,9 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
|
||||
"""
|
||||
if AlternativeBookNamesDB.cursor is None:
|
||||
file_path = AppLocation.get_directory(AppLocation.DataDir) / 'bibles' / 'alternative_book_names.sqlite'
|
||||
exists = file_path.exists()
|
||||
AlternativeBookNamesDB.conn = sqlite3.connect(str(file_path))
|
||||
if not file_path.exists():
|
||||
if not exists:
|
||||
# create new DB, create table alternative_book_names
|
||||
AlternativeBookNamesDB.conn.execute(
|
||||
'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, '
|
||||
|
@ -51,7 +51,7 @@ class WordProjectBible(BibleImport):
|
||||
Unzip the file to a temporary directory
|
||||
"""
|
||||
self.tmp = TemporaryDirectory()
|
||||
with ZipFile(str(self.file_path)) as zip_file:
|
||||
with ZipFile(self.file_path) as zip_file:
|
||||
zip_file.extractall(self.tmp.name)
|
||||
self.base_path = Path(self.tmp.name, self.file_path.stem)
|
||||
|
||||
|
@ -187,7 +187,7 @@ class BibleManager(LogMixin, RegistryProperties):
|
||||
bible = self.db_cache[name]
|
||||
bible.session.close_all()
|
||||
bible.session = None
|
||||
return delete_file(Path(bible.path, bible.file))
|
||||
return delete_file(bible.path, bible.file_path)
|
||||
|
||||
def get_bibles(self):
|
||||
"""
|
||||
|
@ -911,16 +911,16 @@ class BibleMediaItem(MediaManagerItem):
|
||||
list_widget_items.append(bible_verse)
|
||||
return list_widget_items
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service):
|
||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
||||
**kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service item to be built on
|
||||
:param item: The Song item to be used
|
||||
:param xml_version: The xml version (not used)
|
||||
:param remote: Triggered from remote
|
||||
:param context: Why is it being generated
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
log.debug('generating slide data')
|
||||
if item:
|
||||
|
@ -28,7 +28,7 @@ from sqlalchemy.sql import and_, func, or_
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import ServiceItemContext, check_item_selected
|
||||
from openlp.core.lib import check_item_selected
|
||||
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
||||
from openlp.core.lib.plugin import PluginStatus
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||
@ -219,15 +219,12 @@ class CustomMediaItem(MediaManagerItem):
|
||||
self.search_text_edit.setFocus()
|
||||
self.search_text_edit.selectAll()
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False,
|
||||
remote=False, context=ServiceItemContext.Service):
|
||||
def generate_slide_data(self, service_item, *, item=None, **kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
:param service_item: To be updated
|
||||
:param item: The custom database item to be used
|
||||
:param xml_version: No used
|
||||
:param remote: Is this triggered by the Preview Controller or Service Manager.
|
||||
:param context: Why is this item required to be build (Default Service).
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
item_id = self._get_id_of_item_to_generate(item, self.remote_custom)
|
||||
service_item.add_capability(ItemCapabilities.CanEdit)
|
||||
|
@ -542,16 +542,16 @@ class ImageMediaItem(MediaManagerItem):
|
||||
image_items.sort(key=lambda item: get_natural_key(item.text(0)))
|
||||
target_group.addChildren(image_items)
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service):
|
||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
||||
**kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service item to be built on
|
||||
:param item: The Song item to be used
|
||||
:param xml_version: The xml version (not used)
|
||||
:param remote: Triggered from remote
|
||||
:param context: Why is it being generated
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
background = QtGui.QColor(Settings().value(self.settings_section + '/background color'))
|
||||
if item:
|
||||
|
@ -29,7 +29,7 @@ from openlp.core.state import State
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import Path, create_paths, path_to_str
|
||||
from openlp.core.common.path import create_paths, path_to_str
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import MediaType, ServiceItemContext, check_item_selected
|
||||
@ -166,16 +166,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
# self.display_type_combo_box.currentIndexChanged.connect(self.override_player_changed)
|
||||
pass
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service):
|
||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
||||
**kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service item to be built on
|
||||
:param item: The Song item to be used
|
||||
:param xml_version: The xml version (not used)
|
||||
:param remote: Triggered from remote
|
||||
:param context: Why is it being generated
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
if item is None:
|
||||
item = self.list_view.currentItem()
|
||||
@ -229,8 +229,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
Initialize media item.
|
||||
"""
|
||||
self.list_view.clear()
|
||||
self.service_path = str(AppLocation.get_section_data_path(self.settings_section) / 'thumbnails')
|
||||
create_paths(Path(self.service_path))
|
||||
self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
|
||||
create_paths(self.service_path)
|
||||
self.load_list([path_to_str(file) for file in Settings().value(self.settings_section + '/media files')])
|
||||
self.rebuild_players()
|
||||
|
||||
@ -264,7 +264,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
:param media: The media
|
||||
:param target_group:
|
||||
"""
|
||||
media.sort(key=lambda file_name: get_natural_key(os.path.split(str(file_name))[1]))
|
||||
media.sort(key=lambda file_path: get_natural_key(file_path.name))
|
||||
for track in media:
|
||||
track_info = QtCore.QFileInfo(track)
|
||||
item_name = None
|
||||
|
@ -46,6 +46,7 @@ from openlp.plugins.presentations.lib.presentationcontroller import Presentation
|
||||
if is_win():
|
||||
from win32com.client import Dispatch
|
||||
import pywintypes
|
||||
uno_available = False
|
||||
# Declare an empty exception to match the exception imported from UNO
|
||||
|
||||
class ErrorCodeIOException(Exception):
|
||||
|
@ -260,16 +260,16 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
doc.presentation_deleted()
|
||||
doc.close_presentation()
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service, file_path=None):
|
||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
||||
file_path=None, **kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service item to be built on
|
||||
:param item: The Song item to be used
|
||||
:param xml_version: The xml version (not used)
|
||||
:param remote: Triggered from remote
|
||||
:param context: Why is it being generated
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
if item:
|
||||
items = [item]
|
||||
|
@ -337,14 +337,8 @@ class MessageListener(object):
|
||||
# Create a copy of the original item, and then clear the original item so it can be filled with images
|
||||
item_cpy = copy.copy(item)
|
||||
item.__init__(None)
|
||||
if is_live:
|
||||
# TODO: To Path object
|
||||
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live,
|
||||
str(file_path))
|
||||
else:
|
||||
# TODO: To Path object
|
||||
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview,
|
||||
str(file_path))
|
||||
context = ServiceItemContext.Live if is_live else ServiceItemContext.Preview
|
||||
self.media_item.generate_slide_data(item, item=item_cpy, context=context, file_path=file_path)
|
||||
# Some of the original serviceitem attributes is needed in the new serviceitem
|
||||
item.footer = item_cpy.footer
|
||||
item.from_service = item_cpy.from_service
|
||||
|
@ -329,8 +329,13 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
|
||||
importer = self.plugin.import_songs(
|
||||
source_format,
|
||||
file_paths=self.get_list_of_paths(self.format_widgets[source_format]['file_list_widget']))
|
||||
importer.do_import()
|
||||
self.progress_label.setText(WizardStrings.FinishedImport)
|
||||
try:
|
||||
importer.do_import()
|
||||
self.progress_label.setText(WizardStrings.FinishedImport)
|
||||
except OSError as e:
|
||||
log.exception('Importing songs failed')
|
||||
self.progress_label.setText(translate('SongsPlugin.ImportWizardForm',
|
||||
'Your Song import failed. {error}').format(error=e))
|
||||
|
||||
def on_error_copy_to_button_clicked(self):
|
||||
"""
|
||||
|
@ -263,8 +263,9 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
||||
self.login_progress_bar.setVisible(True)
|
||||
self.application.process_events()
|
||||
# Log the user in
|
||||
if not self.song_select_importer.login(
|
||||
self.username_edit.text(), self.password_edit.text(), self._update_login_progress):
|
||||
subscription_level = self.song_select_importer.login(
|
||||
self.username_edit.text(), self.password_edit.text(), self._update_login_progress)
|
||||
if not subscription_level:
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
|
||||
@ -272,6 +273,14 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
|
||||
'There was a problem logging in, perhaps your username or password is incorrect?')
|
||||
)
|
||||
else:
|
||||
if subscription_level == 'Free':
|
||||
QtWidgets.QMessageBox.information(
|
||||
self,
|
||||
translate('SongsPlugin.SongSelectForm', 'Free user'),
|
||||
translate('SongsPlugin.SongSelectForm', 'You logged in with a free account, '
|
||||
'the search will be limited to songs '
|
||||
'in the public domain.')
|
||||
)
|
||||
if self.save_password_checkbox.isChecked():
|
||||
Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text())
|
||||
Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text())
|
||||
|
@ -67,7 +67,7 @@ class CCLIFileImport(SongImport):
|
||||
details = {'confidence': 1, 'encoding': 'utf-8'}
|
||||
except UnicodeDecodeError:
|
||||
details = chardet.detect(detect_content)
|
||||
in_file = codecs.open(str(file_path), 'r', details['encoding'])
|
||||
in_file = codecs.open(file_path, 'r', details['encoding'])
|
||||
if not in_file.read(1) == '\ufeff':
|
||||
# not UTF or no BOM was found
|
||||
in_file.seek(0)
|
||||
@ -251,10 +251,10 @@ class CCLIFileImport(SongImport):
|
||||
line_number = 0
|
||||
check_first_verse_line = False
|
||||
verse_text = ''
|
||||
verse_type = VerseType.tags[VerseType.Verse]
|
||||
song_author = ''
|
||||
verse_start = False
|
||||
for line in text_list:
|
||||
verse_type = 'v'
|
||||
clean_line = line.strip()
|
||||
if not clean_line:
|
||||
if line_number == 0:
|
||||
@ -263,6 +263,7 @@ class CCLIFileImport(SongImport):
|
||||
if verse_text:
|
||||
self.add_verse(verse_text, verse_type)
|
||||
verse_text = ''
|
||||
verse_type = VerseType.tags[VerseType.Verse]
|
||||
verse_start = False
|
||||
else:
|
||||
# line_number=0, song title
|
||||
@ -279,7 +280,7 @@ class CCLIFileImport(SongImport):
|
||||
elif not verse_start:
|
||||
# We have the verse descriptor
|
||||
verse_desc_parts = clean_line.split(' ')
|
||||
if len(verse_desc_parts) == 2:
|
||||
if len(verse_desc_parts):
|
||||
if verse_desc_parts[0].startswith('Ver'):
|
||||
verse_type = VerseType.tags[VerseType.Verse]
|
||||
elif verse_desc_parts[0].startswith('Ch'):
|
||||
|
@ -557,16 +557,14 @@ class SongMediaItem(MediaManagerItem):
|
||||
self.plugin.manager.save_object(new_song)
|
||||
self.on_song_list_load()
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service):
|
||||
def generate_slide_data(self, service_item, *, item=None, context=ServiceItemContext.Service, **kwargs):
|
||||
"""
|
||||
Generate the slide data. Needs to be implemented by the plugin.
|
||||
|
||||
:param service_item: The service item to be built on
|
||||
:param item: The Song item to be used
|
||||
:param xml_version: The xml version (not used)
|
||||
:param remote: Triggered from remote
|
||||
:param context: Why is it being generated
|
||||
:param kwargs: Consume other unused args specified by the base implementation, but not use by this one.
|
||||
"""
|
||||
log.debug('generate_slide_data: {service}, {item}, {remote}'.format(service=service_item, item=item,
|
||||
remote=self.remote_song))
|
||||
|
@ -81,7 +81,7 @@ class SongSelectImport(object):
|
||||
:param username: SongSelect username
|
||||
:param password: SongSelect password
|
||||
:param callback: Method to notify of progress.
|
||||
:return: True on success, False on failure.
|
||||
:return: subscription level on success, None on failure.
|
||||
"""
|
||||
if callback:
|
||||
callback()
|
||||
@ -115,11 +115,31 @@ class SongSelectImport(object):
|
||||
return False
|
||||
if callback:
|
||||
callback()
|
||||
# Page if user is in an organization
|
||||
if posted_page.find('input', id='SearchText') is not None:
|
||||
return True
|
||||
self.subscription_level = self.find_subscription_level(posted_page)
|
||||
return self.subscription_level
|
||||
# Page if user is not in an organization
|
||||
elif posted_page.find('div', id="select-organization") is not None:
|
||||
try:
|
||||
home_page = BeautifulSoup(self.opener.open(BASE_URL).read(), 'lxml')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not reach SongSelect, {error}'.format(error=error))
|
||||
self.subscription_level = self.find_subscription_level(home_page)
|
||||
return self.subscription_level
|
||||
else:
|
||||
log.debug(posted_page)
|
||||
return False
|
||||
return None
|
||||
|
||||
def find_subscription_level(self, page):
|
||||
scripts = page.find_all('script')
|
||||
for tag in scripts:
|
||||
if tag.string:
|
||||
match = re.search("'Subscription': '(?P<subscription_level>[^']+)", tag.string)
|
||||
if match:
|
||||
return match.group('subscription_level')
|
||||
log.error('Could not determine SongSelect subscription level')
|
||||
return None
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
@ -128,7 +148,7 @@ class SongSelectImport(object):
|
||||
try:
|
||||
self.opener.open(LOGOUT_URL)
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not log of SongSelect, {error}'.format(error=error))
|
||||
log.exception('Could not log out of SongSelect, {error}'.format(error=error))
|
||||
|
||||
def search(self, search_text, max_results, callback=None):
|
||||
"""
|
||||
@ -145,7 +165,7 @@ class SongSelectImport(object):
|
||||
'PrimaryLanguage': '',
|
||||
'Keys': '',
|
||||
'Themes': '',
|
||||
'List': '',
|
||||
'List': 'publicdomain' if self.subscription_level == 'Free' else '',
|
||||
'Sort': '',
|
||||
'SearchText': search_text
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
||||
self.main_window.error_message(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'You have not set a valid output location for your'
|
||||
' song usage report. \nPlease select an existing path on your computer.')
|
||||
' song usage report.\nPlease select an existing path on your computer.')
|
||||
)
|
||||
return
|
||||
create_paths(path)
|
||||
@ -112,7 +112,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
||||
self.main_window.information_message(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
|
||||
translate('SongUsagePlugin.SongUsageDetailForm',
|
||||
'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
|
||||
'Report\n{name}\nhas been successfully created.').format(name=report_file_name)
|
||||
)
|
||||
except OSError as ose:
|
||||
log.exception('Failed to write out song usage records')
|
||||
|
@ -24,6 +24,7 @@
|
||||
The entrypoint for OpenLP
|
||||
"""
|
||||
import faulthandler
|
||||
import logging
|
||||
import multiprocessing
|
||||
import sys
|
||||
|
||||
@ -34,14 +35,19 @@ from openlp.core.common import is_macosx, is_win
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.path import create_paths
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_up_fault_handling():
|
||||
"""
|
||||
Set up the Python fault handler
|
||||
"""
|
||||
# Create the cache directory if it doesn't exist, and enable the fault handler to log to an error log file
|
||||
create_paths(AppLocation.get_directory(AppLocation.CacheDir))
|
||||
faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb'))
|
||||
try:
|
||||
create_paths(AppLocation.get_directory(AppLocation.CacheDir))
|
||||
faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb'))
|
||||
except OSError:
|
||||
log.exception('An exception occurred when enabling the fault handler')
|
||||
|
||||
|
||||
def start():
|
||||
|
@ -1,5 +1,8 @@
|
||||
version: OpenLP-win-ci-b{build}
|
||||
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
|
||||
clone_script:
|
||||
- curl -L https://bazaar.launchpad.net/BRANCHPATH/tarball -o sourcecode.tar.gz
|
||||
- 7z e sourcecode.tar.gz
|
||||
@ -7,15 +10,17 @@ clone_script:
|
||||
- mv BRANCHPATH openlp-branch
|
||||
|
||||
environment:
|
||||
PYTHON: C:\\Python37-x64
|
||||
matrix:
|
||||
- PYTHON: C:\\Python37-x64
|
||||
- PYTHON: C:\\Python37
|
||||
|
||||
install:
|
||||
# Install dependencies from pypi
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python nose mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 pymediainfo"
|
||||
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo"
|
||||
# Download and unpack mupdf
|
||||
- appveyor DownloadFile https://mupdf.com/downloads/archive/mupdf-1.14.0-windows.zip
|
||||
- 7z x mupdf-1.14.0-windows.zip
|
||||
- cp mupdf-1.14.0-windows/mupdf.exe openlp-branch/mupdf.exe
|
||||
- cp mupdf-1.14.0-windows/mutool.exe openlp-branch/mutool.exe
|
||||
# Download and unpack mediainfo
|
||||
- appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/18.08.1/MediaInfo_CLI_18.08.1_Windows_i386.zip
|
||||
- mkdir MediaInfo
|
||||
@ -27,7 +32,7 @@ build: off
|
||||
test_script:
|
||||
- cd openlp-branch
|
||||
# Run the tests
|
||||
- "%PYTHON%\\python.exe -m nose -v tests"
|
||||
- "%PYTHON%\\python.exe -m pytest -v tests"
|
||||
# Go back to the user root folder
|
||||
- cd..
|
||||
|
||||
|
@ -9,6 +9,7 @@ ignore = E402,E722,W503,W504
|
||||
|
||||
[flake8]
|
||||
exclude=resources.py,vlc.py
|
||||
max-line-length = 120
|
||||
ignore = E402,W503,W504,D
|
||||
|
||||
[pycodestyle]
|
||||
|
@ -15,13 +15,12 @@
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
|
||||
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
from tempfile import mkdtemp
|
||||
# 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 #
|
||||
###############################################################################
|
||||
import os
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@ -57,14 +56,15 @@ class TestRemoteDeploy(TestCase):
|
||||
# GIVEN: A new downloaded zip file
|
||||
mocked_zipfile = MagicMock()
|
||||
MockZipFile.return_value = mocked_zipfile
|
||||
root_path = Path('/tmp/remotes')
|
||||
root_path_str = '{sep}tmp{sep}remotes'.format(sep=os.sep)
|
||||
root_path = Path(root_path_str)
|
||||
|
||||
# WHEN: deploy_zipfile() is called
|
||||
deploy_zipfile(root_path, 'site.zip')
|
||||
|
||||
# THEN: the zip file should have been extracted to the right location
|
||||
MockZipFile.assert_called_once_with('/tmp/remotes/site.zip')
|
||||
mocked_zipfile.extractall.assert_called_once_with('/tmp/remotes')
|
||||
MockZipFile.assert_called_once_with(Path('/tmp/remotes/site.zip'))
|
||||
mocked_zipfile.extractall.assert_called_once_with(Path('/tmp/remotes'))
|
||||
|
||||
@patch('openlp.core.api.deploy.Registry')
|
||||
@patch('openlp.core.api.deploy.get_web_page')
|
||||
|
@ -309,9 +309,9 @@ class TestInit(TestCase, TestMixin):
|
||||
"""
|
||||
# GIVEN: A mocked UniversalDetector instance with done attribute set to True after first iteration
|
||||
with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
|
||||
patch.object(Path, 'open', return_value=BytesIO(b"data" * 260)) as mocked_open:
|
||||
patch.object(Path, 'open', return_value=BytesIO(b'data' * 260)) as mocked_open:
|
||||
encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
|
||||
mocked_universal_detector_inst = MagicMock(result=encoding_result)
|
||||
mocked_universal_detector_inst = MagicMock(**{'close.return_value': encoding_result})
|
||||
type(mocked_universal_detector_inst).done = PropertyMock(side_effect=[False, True])
|
||||
mocked_universal_detector.return_value = mocked_universal_detector_inst
|
||||
|
||||
@ -320,7 +320,7 @@ class TestInit(TestCase, TestMixin):
|
||||
|
||||
# THEN: The feed method of UniversalDetector should only br called once before returning a result
|
||||
mocked_open.assert_called_once_with('rb')
|
||||
assert mocked_universal_detector_inst.feed.mock_calls == [call(b"data" * 256)]
|
||||
assert mocked_universal_detector_inst.feed.mock_calls == [call(b'data' * 256)]
|
||||
mocked_universal_detector_inst.close.assert_called_once_with()
|
||||
assert result == encoding_result
|
||||
|
||||
@ -331,10 +331,10 @@ class TestInit(TestCase, TestMixin):
|
||||
# GIVEN: A mocked UniversalDetector instance which isn't set to done and a mocked open, with 1040 bytes of test
|
||||
# data (enough to run the iterator twice)
|
||||
with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
|
||||
patch.object(Path, 'open', return_value=BytesIO(b"data" * 260)) as mocked_open:
|
||||
patch.object(Path, 'open', return_value=BytesIO(b'data' * 260)) as mocked_open:
|
||||
encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
|
||||
mocked_universal_detector_inst = MagicMock(mock=mocked_universal_detector,
|
||||
**{'done': False, 'result': encoding_result})
|
||||
**{'done': False, 'close.return_value': encoding_result})
|
||||
mocked_universal_detector.return_value = mocked_universal_detector_inst
|
||||
|
||||
# WHEN: Calling get_file_encoding
|
||||
@ -342,7 +342,7 @@ class TestInit(TestCase, TestMixin):
|
||||
|
||||
# THEN: The feed method of UniversalDetector should have been called twice before returning a result
|
||||
mocked_open.assert_called_once_with('rb')
|
||||
assert mocked_universal_detector_inst.feed.mock_calls == [call(b"data" * 256), call(b"data" * 4)]
|
||||
assert mocked_universal_detector_inst.feed.mock_calls == [call(b'data' * 256), call(b'data' * 4)]
|
||||
mocked_universal_detector_inst.close.assert_called_once_with()
|
||||
assert result == encoding_result
|
||||
|
||||
@ -352,13 +352,19 @@ class TestInit(TestCase, TestMixin):
|
||||
"""
|
||||
# GIVEN: A mocked UniversalDetector instance which isn't set to done and a mocked open, with 1040 bytes of test
|
||||
# data (enough to run the iterator twice)
|
||||
with patch('openlp.core.common.UniversalDetector'), \
|
||||
with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
|
||||
patch('builtins.open', side_effect=OSError), \
|
||||
patch('openlp.core.common.log') as mocked_log:
|
||||
encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
|
||||
mocked_universal_detector_inst = MagicMock(mock=mocked_universal_detector,
|
||||
**{'done': False, 'close.return_value': encoding_result})
|
||||
mocked_universal_detector.return_value = mocked_universal_detector_inst
|
||||
|
||||
# WHEN: Calling get_file_encoding
|
||||
result = get_file_encoding(Path('file name'))
|
||||
|
||||
# THEN: log.exception should be called and get_file_encoding should return None
|
||||
mocked_log.exception.assert_called_once_with('Error detecting file encoding')
|
||||
assert result is None
|
||||
mocked_universal_detector_inst.feed.assert_not_called()
|
||||
mocked_universal_detector_inst.close.assert_called_once_with()
|
||||
assert result == encoding_result
|
||||
|
@ -179,9 +179,8 @@ class TestShutil(TestCase):
|
||||
# WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type
|
||||
path.rmtree()
|
||||
|
||||
# THEN: :func:`shutil.rmtree` should have been called with the str equivalents of the Path object.
|
||||
mocked_shutil_rmtree.assert_called_once_with(
|
||||
os.path.join('test', 'path'), False, None)
|
||||
# THEN: :func:`shutil.rmtree` should have been called with the the Path object.
|
||||
mocked_shutil_rmtree.assert_called_once_with(Path('test', 'path'), False, None)
|
||||
|
||||
def test_rmtree_optional_params(self):
|
||||
"""
|
||||
@ -198,8 +197,7 @@ class TestShutil(TestCase):
|
||||
|
||||
# THEN: :func:`shutil.rmtree` should have been called with the optional parameters, with out any of the
|
||||
# values being modified
|
||||
mocked_shutil_rmtree.assert_called_once_with(
|
||||
os.path.join('test', 'path'), True, mocked_on_error)
|
||||
mocked_shutil_rmtree.assert_called_once_with(path, True, mocked_on_error)
|
||||
|
||||
def test_which_no_command(self):
|
||||
"""
|
||||
@ -324,7 +322,7 @@ class TestPath(TestCase):
|
||||
obj = path.json_object(extra=1, args=2)
|
||||
|
||||
# THEN: A JSON decodable object should have been returned.
|
||||
assert obj == {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')}
|
||||
assert obj == {'__Path__': (os.sep, 'base', 'path', 'to', 'fi.le')}
|
||||
|
||||
def test_path_json_object_base_path(self):
|
||||
"""
|
||||
|
@ -178,7 +178,7 @@ class TestImageManager(TestCase, TestMixin):
|
||||
# THEN a KeyError is thrown
|
||||
with self.assertRaises(KeyError) as context:
|
||||
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
|
||||
assert context.exception is not '', 'KeyError exception should have been thrown for missing image'
|
||||
assert context.exception != '', 'KeyError exception should have been thrown for missing image'
|
||||
|
||||
@patch('openlp.core.lib.imagemanager.run_thread')
|
||||
def test_different_dimension_image(self, mocked_run_thread):
|
||||
@ -211,7 +211,7 @@ class TestImageManager(TestCase, TestMixin):
|
||||
# WHEN: calling with correct image, but wrong dimensions
|
||||
with self.assertRaises(KeyError) as context:
|
||||
self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
|
||||
assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension'
|
||||
assert context.exception != '', 'KeyError exception should have been thrown for missing dimension'
|
||||
|
||||
@patch('openlp.core.lib.imagemanager.resize_image')
|
||||
@patch('openlp.core.lib.imagemanager.image_to_byte')
|
||||
|
@ -141,7 +141,7 @@ class TestServiceItem(TestCase, TestMixin):
|
||||
"""
|
||||
# GIVEN: A new service item and a mocked add icon function
|
||||
image_name = 'image_1.jpg'
|
||||
test_file = os.path.join(str(TEST_PATH), image_name)
|
||||
test_file = TEST_PATH / image_name
|
||||
frame_array = {'path': test_file, 'title': image_name}
|
||||
|
||||
service_item = ServiceItem(None)
|
||||
@ -154,13 +154,13 @@ class TestServiceItem(TestCase, TestMixin):
|
||||
mocked_get_section_data_path:
|
||||
mocked_exists.return_value = True
|
||||
mocked_get_section_data_path.return_value = Path('/path/')
|
||||
service_item.set_from_service(line, str(TEST_PATH))
|
||||
service_item.set_from_service(line, TEST_PATH)
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert service_item.is_valid is True, 'The new service item should be valid'
|
||||
assert test_file == service_item.get_rendered_frame(0), 'The first frame should match the path to the image'
|
||||
assert frame_array == service_item.get_frames()[0], 'The return should match frame array1'
|
||||
assert test_file == str(service_item.get_frame_path(0)), \
|
||||
assert test_file == service_item.get_frame_path(0), \
|
||||
'The frame path should match the full path to the image'
|
||||
assert image_name == service_item.get_frame_title(0), 'The frame title should match the image name'
|
||||
assert image_name == service_item.get_display_title(), 'The display title should match the first image name'
|
||||
@ -328,7 +328,7 @@ class TestServiceItem(TestCase, TestMixin):
|
||||
|
||||
# WHEN: We add a custom from a saved service
|
||||
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
|
||||
service_item.set_from_service(line, '/test/')
|
||||
service_item.set_from_service(line, Path('/test/'))
|
||||
|
||||
# THEN: We should get back a valid service item
|
||||
assert service_item.is_valid is True, 'The new service item should be valid'
|
||||
|
@ -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')
|
||||
|
@ -66,9 +66,9 @@ class TestThemeManager(TestCase):
|
||||
theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default')
|
||||
|
||||
# THEN: The zipfile should be created at the given path
|
||||
mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
|
||||
mocked_zipfile_write.assert_called_with(str(RESOURCE_PATH / 'themes' / 'Default' / 'Default.xml'),
|
||||
os.path.join('Default', 'Default.xml'))
|
||||
mocked_zipfile_init.assert_called_with(Path('some', 'path', 'Default.otz'), 'w')
|
||||
mocked_zipfile_write.assert_called_with(RESOURCE_PATH / 'themes' / 'Default' / 'Default.xml',
|
||||
Path('Default', 'Default.xml'))
|
||||
|
||||
def test_initial_theme_manager(self):
|
||||
"""
|
||||
|
@ -55,7 +55,8 @@ class TestManager(TestCase):
|
||||
instance = BibleManager(MagicMock())
|
||||
# We need to keep a reference to the mock for close_all as it gets set to None later on!
|
||||
mocked_close_all = MagicMock()
|
||||
mocked_bible = MagicMock(file='KJV.sqlite', path='bibles', **{'session.close_all': mocked_close_all})
|
||||
mocked_bible = MagicMock(file_path='KJV.sqlite', path=Path('bibles'),
|
||||
**{'session.close_all': mocked_close_all})
|
||||
instance.db_cache = {'KJV': mocked_bible}
|
||||
|
||||
# WHEN: Calling delete_bible with 'KJV'
|
||||
@ -66,4 +67,4 @@ class TestManager(TestCase):
|
||||
assert result is True
|
||||
mocked_close_all.assert_called_once_with()
|
||||
assert mocked_bible.session is None
|
||||
mocked_delete_file.assert_called_once_with(Path('bibles', 'KJV.sqlite'))
|
||||
mocked_delete_file.assert_called_once_with(Path('bibles'), 'KJV.sqlite')
|
||||
|
@ -57,4 +57,4 @@ class MediaPluginTest(TestCase, TestMixin):
|
||||
# THEN: about() should return a string object
|
||||
assert isinstance(MediaPlugin.about(), str)
|
||||
# THEN: about() should return a non-empty string
|
||||
assert len(MediaPlugin.about()) is not 0
|
||||
assert len(MediaPlugin.about()) != 0
|
||||
|
@ -199,7 +199,7 @@ class TestLib(TestCase):
|
||||
result = _remove_typos(diff)
|
||||
|
||||
# THEN: There should be no typos in the middle anymore. The remaining equals should have been merged.
|
||||
assert len(result) is 1, 'The result should contain only one element.'
|
||||
assert len(result) == 1, 'The result should contain only one element.'
|
||||
assert result[0][0] == 'equal', 'The result should contain an equal element.'
|
||||
assert result[0][1] == 0, 'The start indices should be kept.'
|
||||
assert result[0][2] == 22, 'The stop indices should be kept.'
|
||||
|
@ -64,7 +64,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
|
||||
def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener):
|
||||
"""
|
||||
Test that when logging in to SongSelect fails, the login method returns False
|
||||
Test that when logging in to SongSelect fails, the login method returns None
|
||||
"""
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
mocked_opener = MagicMock()
|
||||
@ -80,12 +80,12 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
result = importer.login('username', 'password', mock_callback)
|
||||
|
||||
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned
|
||||
# THEN: callback was called 3 times, open was called twice, find was called twice, and None was returned
|
||||
assert 3 == mock_callback.call_count, 'callback should have been called 3 times'
|
||||
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice'
|
||||
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once'
|
||||
assert 2 == mocked_posted_page.find.call_count, 'find should have been called twice'
|
||||
assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
|
||||
assert result is False, 'The login method should have returned False'
|
||||
assert result is None, 'The login method should have returned None'
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.build_opener')
|
||||
def test_login_except(self, mocked_build_opener):
|
||||
@ -129,7 +129,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
|
||||
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
|
||||
assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
|
||||
assert result is True, 'The login method should have returned True'
|
||||
assert result is None, 'The login method should have returned the subscription level'
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.build_opener')
|
||||
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
|
||||
@ -146,7 +146,8 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form]
|
||||
mocked_posted_page = MagicMock()
|
||||
mocked_posted_page.find.return_value = MagicMock()
|
||||
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
|
||||
mocked_home_page = MagicMock()
|
||||
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page, mocked_home_page]
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
|
||||
@ -158,7 +159,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
|
||||
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
|
||||
assert 'https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0]
|
||||
assert result is True, 'The login method should have returned True'
|
||||
assert result is None, 'The login method should have returned the subscription level'
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.build_opener')
|
||||
def test_logout(self, mocked_build_opener):
|
||||
@ -191,6 +192,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
importer.subscription_level = 'premium'
|
||||
|
||||
# WHEN: The login method is called after being rigged to fail
|
||||
results = importer.search('text', 1000, mock_callback)
|
||||
@ -231,6 +233,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
importer.subscription_level = 'premium'
|
||||
|
||||
# WHEN: The search method is called
|
||||
results = importer.search('text', 1000, mock_callback)
|
||||
@ -282,6 +285,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
MockedBeautifulSoup.return_value = mocked_results_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
importer.subscription_level = 'premium'
|
||||
|
||||
# WHEN: The search method is called
|
||||
results = importer.search('text', 2, mock_callback)
|
||||
|
@ -45,8 +45,8 @@ class TestSongUsage(TestCase):
|
||||
# THEN: about() should return a string object
|
||||
assert isinstance(SongUsagePlugin.about(), str)
|
||||
# THEN: about() should return a non-empty string
|
||||
assert len(SongUsagePlugin.about()) is not 0
|
||||
assert len(SongUsagePlugin.about()) is not 0
|
||||
assert len(SongUsagePlugin.about()) != 0
|
||||
assert len(SongUsagePlugin.about()) != 0
|
||||
|
||||
@patch('openlp.plugins.songusage.songusageplugin.Manager')
|
||||
def test_song_usage_init(self, MockedManager):
|
||||
|
88
tests/interfaces/openlp_core/ui/test_firsttimeform.py
Normal file
88
tests/interfaces/openlp_core/ui/test_firsttimeform.py
Normal 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())])
|
@ -236,8 +236,8 @@ class TestSongMaintenanceForm(TestCase, TestMixin):
|
||||
assert MockedQListWidgetItem.call_args_list == expected_widget_item_calls, MockedQListWidgetItem.call_args_list
|
||||
mocked_author_item1.setData.assert_called_once_with(QtCore.Qt.UserRole, 2)
|
||||
mocked_author_item2.setData.assert_called_once_with(QtCore.Qt.UserRole, 1)
|
||||
mocked_authors_list_widget.addItem.call_args_list == [
|
||||
call(mocked_author_item1), call(mocked_author_item2)]
|
||||
mocked_authors_list_widget.addItem.assert_has_calls([
|
||||
call(mocked_author_item1), call(mocked_author_item2)])
|
||||
|
||||
@patch('openlp.plugins.songs.forms.songmaintenanceform.QtWidgets.QListWidgetItem')
|
||||
@patch('openlp.plugins.songs.forms.songmaintenanceform.Topic')
|
||||
|
@ -70,7 +70,7 @@ class FakeIP4InterfaceEntry(QObject):
|
||||
"""
|
||||
Return a QFlags enum with IsUp and IsRunning
|
||||
"""
|
||||
return (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)
|
||||
return QNetworkInterface.IsUp | QNetworkInterface.IsRunning
|
||||
|
||||
def name(self):
|
||||
return self.my_name
|
||||
|
@ -134,6 +134,7 @@ class TestProjectorDB(TestCase, TestMixin):
|
||||
"""
|
||||
Set up anything necessary for all tests
|
||||
"""
|
||||
self.tmp_folder = mkdtemp(prefix='openlp_')
|
||||
# Create a test app to keep from segfaulting
|
||||
Registry.create()
|
||||
self.registry = Registry()
|
||||
@ -153,11 +154,11 @@ class TestProjectorDB(TestCase, TestMixin):
|
||||
patch('openlp.core.ui.mainwindow.ThemeManager'), \
|
||||
patch('openlp.core.ui.mainwindow.ProjectorManager'), \
|
||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
|
||||
patch('openlp.core.ui.mainwindow.server.HttpServer'):
|
||||
patch('openlp.core.ui.mainwindow.server.HttpServer'), \
|
||||
patch('openlp.core.state.State.list_plugins') as mock_plugins:
|
||||
mock_plugins.return_value = []
|
||||
self.main_window = MainWindow()
|
||||
|
||||
# Create a temporary database directory and database
|
||||
self.tmp_folder = mkdtemp(prefix='openlp_')
|
||||
tmpdb_url = 'sqlite:///{db}'.format(db=os.path.join(self.tmp_folder, TEST_DB))
|
||||
mocked_init_url.return_value = tmpdb_url
|
||||
self.projector = ProjectorDB()
|
||||
|
@ -452,12 +452,16 @@ class TestPJLinkCommands(TestCase):
|
||||
"""
|
||||
Test saving video source available information
|
||||
"""
|
||||
|
||||
# GIVEN: Test object
|
||||
with patch.object(openlp.core.projectors.pjlink, 'log') as mock_log:
|
||||
pjlink = PJLink(Projector(**TEST1_DATA), no_poll=True)
|
||||
pjlink.source_available = []
|
||||
log_debug_calls = [call('({ip}) Setting projector sources_available to '
|
||||
log_debug_calls = [call('({ip}) reset_information() connect status is '
|
||||
'S_NOT_CONNECTED'.format(ip=pjlink.name)),
|
||||
call('({ip}) Setting projector source_available to '
|
||||
'"[\'11\', \'12\', \'21\', \'22\', \'31\', \'32\']"'.format(ip=pjlink.name))]
|
||||
|
||||
chk_data = '21 12 11 22 32 31' # Although they should already be sorted, use unsorted to verify method
|
||||
chk_test = ['11', '12', '21', '22', '31', '32']
|
||||
|
||||
@ -601,9 +605,9 @@ class TestPJLinkCommands(TestCase):
|
||||
|
||||
# THEN: Power should be set to ON
|
||||
assert pjlink.power == S_STANDBY, 'Power should not have changed'
|
||||
assert mock_UpdateIcons.emit.called is False, 'projectorUpdateIcons() should not have been called'
|
||||
mock_change_status.called is False, 'change_status() should not have been called'
|
||||
mock_send_command.called is False, 'send_command() should not have been called'
|
||||
mock_UpdateIcons.emit.assert_not_called()
|
||||
mock_change_status.assert_not_called()
|
||||
mock_send_command.assert_not_called()
|
||||
mock_log.warning.assert_has_calls(log_warn_calls)
|
||||
|
||||
def test_projector_process_powr_off(self):
|
||||
@ -623,9 +627,9 @@ class TestPJLinkCommands(TestCase):
|
||||
|
||||
# THEN: Power should be set to ON
|
||||
assert pjlink.power == S_STANDBY, 'Power should have changed to S_STANDBY'
|
||||
assert mock_UpdateIcons.emit.called is True, 'projectorUpdateIcons should have been called'
|
||||
mock_change_status.called is True, 'change_status should have been called'
|
||||
mock_send_command.called is False, 'send_command should not have been called'
|
||||
mock_UpdateIcons.emit.assert_called_with()
|
||||
mock_change_status.assert_called_with(313)
|
||||
mock_send_command.assert_not_called()
|
||||
|
||||
def test_projector_process_rfil_save(self):
|
||||
"""
|
||||
|
@ -83,8 +83,8 @@ class ProjectorSourceFormTest(TestCase, TestMixin):
|
||||
Delete all C++ objects at end so we don't segfault.
|
||||
"""
|
||||
self.projectordb.session.close()
|
||||
del(self.projectordb)
|
||||
del(self.projector)
|
||||
del self.projectordb
|
||||
del self.projector
|
||||
retries = 0
|
||||
while retries < 5:
|
||||
try:
|
||||
|
@ -24,7 +24,7 @@ Package to test for proper bzr tags.
|
||||
"""
|
||||
import os
|
||||
from subprocess import PIPE, Popen
|
||||
from unittest import TestCase
|
||||
from unittest import TestCase, SkipTest
|
||||
|
||||
|
||||
TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10',
|
||||
@ -42,7 +42,10 @@ class TestBzrTags(TestCase):
|
||||
path = os.path.dirname(__file__)
|
||||
|
||||
# WHEN getting the branches tags
|
||||
bzr = Popen(('bzr', 'tags', '--directory=' + path), stdout=PIPE)
|
||||
try:
|
||||
bzr = Popen(('bzr', 'tags', '--directory=' + path), stdout=PIPE)
|
||||
except Exception:
|
||||
raise SkipTest('bzr is not installed')
|
||||
std_out = bzr.communicate()[0]
|
||||
count = len(TAGS1)
|
||||
tags = [line.decode('utf-8').split()[0] for line in std_out.splitlines()]
|
||||
|
Loading…
Reference in New Issue
Block a user