This commit is contained in:
Raoul Snyman 2017-09-28 10:32:10 -07:00
commit c2e16cc8e9
89 changed files with 2152 additions and 1784 deletions

Binary file not shown.

Binary file not shown.

View File

@ -20,13 +20,18 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
import sys The entrypoint for OpenLP
"""
import faulthandler
import multiprocessing import multiprocessing
import sys
from openlp.core.common import is_win, is_macosx from openlp.core.common import is_win, is_macosx
from openlp.core.common.applocation import AppLocation
from openlp.core import main from openlp.core import main
faulthandler.enable(open(str(AppLocation.get_directory(AppLocation.CacheDir) / 'error.log'), 'wb'))
if __name__ == '__main__': if __name__ == '__main__':
""" """

View File

@ -26,11 +26,8 @@ The :mod:`core` module provides all core application functions
All the core functions of the OpenLP application including the GUI, settings, All the core functions of the OpenLP application including the GUI, settings,
logging and a plugin framework are contained within the openlp.core module. logging and a plugin framework are contained within the openlp.core module.
""" """
import argparse import argparse
import logging import logging
import os
import shutil
import sys import sys
import time import time
from datetime import datetime from datetime import datetime
@ -40,8 +37,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, is_macosx, is_win, translate check_directory_exists, is_macosx, is_win, translate
from openlp.core.common.path import Path from openlp.core.common.path import Path, copytree
from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.version import check_for_update, get_version
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from openlp.core.resources import qInitResources from openlp.core.resources import qInitResources
from openlp.core.ui import SplashScreen from openlp.core.ui import SplashScreen
@ -160,8 +157,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
self.processEvents() self.processEvents()
if not has_run_wizard: if not has_run_wizard:
self.main_window.first_time() self.main_window.first_time()
version = VersionThread(self.main_window) if Settings().value('core/update check'):
version.start() check_for_update(self.main_window)
self.main_window.is_display_blank() self.main_window.is_display_blank()
self.main_window.app_startup() self.main_window.app_startup()
return self.exec() return self.exec()
@ -186,25 +183,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
""" """
Check if the data folder path exists. Check if the data folder path exists.
""" """
data_folder_path = str(AppLocation.get_data_path()) data_folder_path = AppLocation.get_data_path()
if not os.path.exists(data_folder_path): if not data_folder_path.exists():
log.critical('Database was not found in: ' + data_folder_path) log.critical('Database was not found in: %s', data_folder_path)
status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'), status = QtWidgets.QMessageBox.critical(
translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}' None, translate('OpenLP', 'Data Directory Error'),
'\n\nThe location of the data folder was ' translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data folder '
'previously changed from the OpenLP\'s ' 'was previously changed from the OpenLP\'s default location. If the data was '
'default location. If the data was stored on ' 'stored on removable device, that device needs to be made available.\n\nYou may '
'removable device, that device needs to be ' 'reset the data location back to the default location, or you can try to make the '
'made available.\n\nYou may reset the data ' 'current location available.\n\nDo you want to reset to the default data location? '
'location back to the default location, ' 'If not, OpenLP will be closed so you can try to fix the the problem.')
'or you can try to make the current location ' .format(path=data_folder_path),
'available.\n\nDo you want to reset to the ' QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
'default data location? If not, OpenLP will be ' QtWidgets.QMessageBox.No)
'closed so you can try to fix the the problem.')
.format(path=data_folder_path),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No)
if status == QtWidgets.QMessageBox.No: if status == QtWidgets.QMessageBox.No:
# If answer was "No", return "True", it will shutdown OpenLP in def main # If answer was "No", return "True", it will shutdown OpenLP in def main
log.info('User requested termination') log.info('User requested termination')
@ -245,7 +237,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
:param can_show_splash: Should OpenLP show the splash screen :param can_show_splash: Should OpenLP show the splash screen
""" """
data_version = Settings().value('core/application version') data_version = Settings().value('core/application version')
openlp_version = get_application_version()['version'] openlp_version = get_version()['version']
# New installation, no need to create backup # New installation, no need to create backup
if not has_run_wizard: if not has_run_wizard:
Settings().setValue('core/application version', openlp_version) Settings().setValue('core/application version', openlp_version)
@ -258,11 +250,11 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
'a backup of the old data folder?'), 'a backup of the old data folder?'),
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes: defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
# Create copy of data folder # Create copy of data folder
data_folder_path = str(AppLocation.get_data_path()) data_folder_path = AppLocation.get_data_path()
timestamp = time.strftime("%Y%m%d-%H%M%S") timestamp = time.strftime("%Y%m%d-%H%M%S")
data_folder_backup_path = data_folder_path + '-' + timestamp data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp)
try: try:
shutil.copytree(data_folder_path, data_folder_backup_path) copytree(data_folder_path, data_folder_backup_path)
except OSError: except OSError:
QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'), QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
translate('OpenLP', 'Backup of the data folder failed!')) translate('OpenLP', 'Backup of the data folder failed!'))
@ -420,7 +412,7 @@ def main(args=None):
Registry.create() Registry.create()
Registry().register('application', application) Registry().register('application', application)
Registry().set_flag('no_web_server', args.no_web_server) Registry().set_flag('no_web_server', args.no_web_server)
application.setApplicationVersion(get_application_version()['version']) application.setApplicationVersion(get_version()['version'])
# Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one # Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
if application.is_already_running(): if application.is_already_running():
sys.exit() sys.exit()

View File

@ -52,7 +52,7 @@ class Poller(RegistryProperties):
'isSecure': Settings().value('api/authentication enabled'), 'isSecure': Settings().value('api/authentication enabled'),
'isAuthorised': False, 'isAuthorised': False,
'chordNotation': Settings().value('songs/chord notation'), 'chordNotation': Settings().value('songs/chord notation'),
'isStagedActive': self.is_stage_active(), 'isStageActive': self.is_stage_active(),
'isLiveActive': self.is_live_active(), 'isLiveActive': self.is_live_active(),
'isChordsActive': self.is_chords_active() 'isChordsActive': self.is_chords_active()
} }

View File

@ -29,7 +29,6 @@ import sys
from openlp.core.common import Settings, is_win, is_macosx from openlp.core.common import Settings, is_win, is_macosx
from openlp.core.common.path import Path from openlp.core.common.path import Path
if not is_win() and not is_macosx(): if not is_win() and not is_macosx():
try: try:
from xdg import BaseDirectory from xdg import BaseDirectory

View File

@ -24,18 +24,12 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
""" """
import hashlib import hashlib
import logging import logging
import os
import platform
import socket
import sys import sys
import subprocess
import time import time
import urllib.error
import urllib.parse
import urllib.request
from http.client import HTTPException
from random import randint from random import randint
import requests
from openlp.core.common import Registry, trace_error_handler from openlp.core.common import Registry, trace_error_handler
log = logging.getLogger(__name__ + '.__init__') log = logging.getLogger(__name__ + '.__init__')
@ -69,33 +63,6 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2 CONNECTION_RETRIES = 2
class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler):
"""
Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248
(Redirecting to urls with special chars)
"""
def redirect_request(self, req, fp, code, msg, headers, new_url):
#
"""
Test if the new_url can be decoded to ascii
:param req:
:param fp:
:param code:
:param msg:
:param headers:
:param new_url:
:return:
"""
try:
new_url.encode('latin1').decode('ascii')
fixed_url = new_url
except Exception:
# The url could not be decoded to ascii, so we do some url encoding
fixed_url = urllib.parse.quote(new_url.encode('latin1').decode('utf-8', 'replace'), safe='/:')
return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url)
def get_user_agent(): def get_user_agent():
""" """
Return a user agent customised for the platform the user is on. Return a user agent customised for the platform the user is on.
@ -107,7 +74,7 @@ def get_user_agent():
return browser_list[random_index] return browser_list[random_index]
def get_web_page(url, header=None, update_openlp=False): def get_web_page(url, headers=None, update_openlp=False, proxies=None):
""" """
Attempts to download the webpage at url and returns that page or None. Attempts to download the webpage at url and returns that page or None.
@ -116,71 +83,36 @@ def get_web_page(url, header=None, update_openlp=False):
:param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded. :param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded.
Defaults to False. Defaults to False.
""" """
# TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a
# proxy_handler, build into an opener and install the opener into urllib2.
# http://docs.python.org/library/urllib2.html
if not url: if not url:
return None return None
# This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 if not headers:
opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) headers = {}
urllib.request.install_opener(opener) if 'user-agent' not in [key.lower() for key in headers.keys()]:
req = urllib.request.Request(url) headers['User-Agent'] = get_user_agent()
if not header or header[0].lower() != 'user-agent':
user_agent = get_user_agent()
req.add_header('User-Agent', user_agent)
if header:
req.add_header(header[0], header[1])
log.debug('Downloading URL = %s' % url) log.debug('Downloading URL = %s' % url)
retries = 0 retries = 0
while retries <= CONNECTION_RETRIES: while retries < CONNECTION_RETRIES:
retries += 1
time.sleep(0.1)
try: try:
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT) response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT))
log.debug('Downloaded page {text}'.format(text=page.geturl())) log.debug('Downloaded page {url}'.format(url=response.url))
break break
except urllib.error.URLError as err: except IOError:
log.exception('URLError on {text}'.format(text=url)) # For now, catch IOError. All requests errors inherit from IOError
log.exception('URLError: {text}'.format(text=err.reason)) log.exception('Unable to connect to {url}'.format(url=url))
page = None response = None
if retries > CONNECTION_RETRIES: if retries >= CONNECTION_RETRIES:
raise raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))
except socket.timeout: retries += 1
log.exception('Socket timeout: {text}'.format(text=url))
page = None
if retries > CONNECTION_RETRIES:
raise
except socket.gaierror:
log.exception('Socket gaierror: {text}'.format(text=url))
page = None
if retries > CONNECTION_RETRIES:
raise
except ConnectionRefusedError:
log.exception('ConnectionRefused: {text}'.format(text=url))
page = None
if retries > CONNECTION_RETRIES:
raise
break
except ConnectionError:
log.exception('Connection error: {text}'.format(text=url))
page = None
if retries > CONNECTION_RETRIES:
raise
except HTTPException:
log.exception('HTTPException error: {text}'.format(text=url))
page = None
if retries > CONNECTION_RETRIES:
raise
except: except:
# Don't know what's happening, so reraise the original # Don't know what's happening, so reraise the original
log.exception('Unknown error when trying to connect to {url}'.format(url=url))
raise raise
if update_openlp: if update_openlp:
Registry().get('application').process_events() Registry().get('application').process_events()
if not page: if not response or not response.text:
log.exception('{text} could not be downloaded'.format(text=url)) log.error('{url} could not be downloaded'.format(url=url))
return None return None
log.debug(page) return response.text
return page
def get_url_file_size(url): def get_url_file_size(url):
@ -192,81 +124,67 @@ def get_url_file_size(url):
retries = 0 retries = 0
while True: while True:
try: try:
site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
meta = site.info() return int(response.headers['Content-Length'])
return int(meta.get("Content-Length")) except IOError:
except urllib.error.URLError:
if retries > CONNECTION_RETRIES: if retries > CONNECTION_RETRIES:
raise raise ConnectionError('Unable to download {url}'.format(url=url))
else: else:
retries += 1 retries += 1
time.sleep(0.1) time.sleep(0.1)
continue continue
def url_get_file(callback, url, f_path, sha256=None): def url_get_file(callback, url, file_path, sha256=None):
"""" """"
Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any
point. Returns False on download error. point. Returns False on download error.
:param callback: the class which needs to be updated :param callback: the class which needs to be updated
:param url: URL to download :param url: URL to download
:param f_path: Destination file :param file_path: Destination file
:param sha256: The check sum value to be checked against the download value :param sha256: The check sum value to be checked against the download value
""" """
block_count = 0 block_count = 0
block_size = 4096 block_size = 4096
retries = 0 retries = 0
log.debug("url_get_file: " + url) log.debug('url_get_file: %s', url)
while True: while retries < CONNECTION_RETRIES:
try: try:
filename = open(f_path, "wb") with file_path.open('wb') as saved_file:
url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True)
if sha256:
hasher = hashlib.sha256()
# Download until finished or canceled.
while not callback.was_cancelled:
data = url_file.read(block_size)
if not data:
break
filename.write(data)
if sha256: if sha256:
hasher.update(data) hasher = hashlib.sha256()
block_count += 1 # Download until finished or canceled.
callback._download_progress(block_count, block_size) for chunk in response.iter_content(chunk_size=block_size):
filename.close() if callback.was_cancelled:
break
saved_file.write(chunk)
if sha256:
hasher.update(chunk)
block_count += 1
callback._download_progress(block_count, block_size)
response.close()
if sha256 and hasher.hexdigest() != sha256: if sha256 and hasher.hexdigest() != sha256:
log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(),
os.remove(f_path) sha256)
if file_path.exists():
file_path.unlink()
return False return False
except (urllib.error.URLError, socket.timeout) as err: break
except IOError:
trace_error_handler(log) trace_error_handler(log)
filename.close()
os.remove(f_path)
if retries > CONNECTION_RETRIES: if retries > CONNECTION_RETRIES:
if file_path.exists():
file_path.unlink()
return False return False
else: else:
retries += 1 retries += 1
time.sleep(0.1) time.sleep(0.1)
continue continue
break if callback.was_cancelled and file_path.exists():
# Delete file if cancelled, it may be a partial file. file_path.unlink()
if callback.was_cancelled:
os.remove(f_path)
return True return True
def ping(host):
"""
Returns True if host responds to a ping request
"""
# Ping parameters as function of OS
ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1"
args = "ping " + " " + ping_str + " " + host
need_sh = False if platform.system().lower() == "windows" else True
# Ping
return subprocess.call(args, shell=need_sh) == 0
__all__ = ['get_web_page'] __all__ = ['get_web_page']

View File

@ -141,7 +141,7 @@ class LanguageManager(object):
if reg_ex.exactMatch(qmf): if reg_ex.exactMatch(qmf):
name = '{regex}'.format(regex=reg_ex.cap(1)) name = '{regex}'.format(regex=reg_ex.cap(1))
LanguageManager.__qm_list__[ LanguageManager.__qm_list__[
'{count:>2i} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name '{count:>2d} {name}'.format(count=counter + 1, name=LanguageManager.language_name(qmf))] = name
@staticmethod @staticmethod
def get_qm_list(): def get_qm_list():

View File

@ -19,6 +19,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import shutil
from contextlib import suppress from contextlib import suppress
from openlp.core.common import is_win from openlp.core.common import is_win
@ -29,6 +30,121 @@ else:
from pathlib import PosixPath as PathVariant from pathlib import PosixPath as PathVariant
def replace_params(args, kwargs, params):
"""
Apply a transformation function to the specified args or kwargs
:param tuple args: Positional arguments
:param dict kwargs: Key Word arguments
:param params: A tuple of tuples with the position and the key word to replace.
:return: The modified positional and keyword arguments
:rtype: tuple[tuple, dict]
Usage:
Take a method with the following signature, and assume we which to apply the str function to arg2:
def method(arg1=None, arg2=None, arg3=None)
As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
would call this function as follows:
replace_params(args, kwargs, ((1, 'arg2', str),))
"""
args = list(args)
for position, key_word, transform in params:
if len(args) > position:
args[position] = transform(args[position])
elif key_word in kwargs:
kwargs[key_word] = transform(kwargs[key_word])
return tuple(args), kwargs
def copy(*args, **kwargs):
"""
Wraps :func:`shutil.copy` so that we can accept Path objects.
:param src openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
:param dst openlp.core.common.path.Path: Takes a Path object which is then converted to a str object
:return: Converts the str object received from :func:`shutil.copy` to a Path or NoneType object
:rtype: openlp.core.common.path.Path | None
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.copy
"""
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
return str_to_path(shutil.copy(*args, **kwargs))
def copyfile(*args, **kwargs):
"""
Wraps :func:`shutil.copyfile` so that we can accept Path objects.
:param openlp.core.common.path.Path src: Takes a Path object which is then converted to a str object
:param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
:return: Converts the str object received from :func:`shutil.copyfile` to a Path or NoneType object
:rtype: openlp.core.common.path.Path | None
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.copyfile
"""
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
return str_to_path(shutil.copyfile(*args, **kwargs))
def copytree(*args, **kwargs):
"""
Wraps :func:shutil.copytree` so that we can accept Path objects.
:param openlp.core.common.path.Path src : Takes a Path object which is then converted to a str object
:param openlp.core.common.path.Path dst: Takes a Path object which is then converted to a str object
:return: Converts the str object received from :func:`shutil.copytree` to a Path or NoneType object
:rtype: openlp.core.common.path.Path | None
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.copytree
"""
args, kwargs = replace_params(args, kwargs, ((0, 'src', path_to_str), (1, 'dst', path_to_str)))
return str_to_path(shutil.copytree(*args, **kwargs))
def rmtree(*args, **kwargs):
"""
Wraps :func:shutil.rmtree` so that we can accept Path objects.
:param openlp.core.common.path.Path path: Takes a Path object which is then converted to a str object
:return: Passes the return from :func:`shutil.rmtree` back
:rtype: None
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.rmtree
"""
args, kwargs = replace_params(args, kwargs, ((0, 'path', path_to_str),))
return shutil.rmtree(*args, **kwargs)
def which(*args, **kwargs):
"""
Wraps :func:shutil.which` so that it return a Path objects.
:rtype: openlp.core.common.Path
See the following link for more information on the other parameters:
https://docs.python.org/3/library/shutil.html#shutil.which
"""
file_name = shutil.which(*args, **kwargs)
if file_name:
return str_to_path(file_name)
return None
def path_to_str(path=None): def path_to_str(path=None):
""" """
A utility function to convert a Path object or NoneType to a string equivalent. A utility function to convert a Path object or NoneType to a string equivalent.

View File

@ -143,7 +143,7 @@ class Registry(object):
log.exception('Exception for function {function}'.format(function=function)) log.exception('Exception for function {function}'.format(function=function))
else: else:
trace_error_handler(log) trace_error_handler(log)
log.error("Event {event} called but not registered".format(event=event)) log.exception('Event {event} called but not registered'.format(event=event))
return results return results
def get_flag(self, key): def get_flag(self, key):

View File

@ -88,9 +88,6 @@ class UiStrings(object):
self.Error = translate('OpenLP.Ui', 'Error') self.Error = translate('OpenLP.Ui', 'Error')
self.Export = translate('OpenLP.Ui', 'Export') self.Export = translate('OpenLP.Ui', 'Export')
self.File = translate('OpenLP.Ui', 'File') self.File = translate('OpenLP.Ui', 'File')
self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
self.FileNotFoundMessage = translate('OpenLP.Ui',
'File {name} not found.\nPlease try selecting it individually.')
self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit') self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
self.Help = translate('OpenLP.Ui', 'Help') self.Help = translate('OpenLP.Ui', 'Help')
self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours') self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')

View File

@ -32,6 +32,7 @@ import math
from PyQt5 import QtCore, QtGui, Qt, QtWidgets from PyQt5 import QtCore, QtGui, Qt, QtWidgets
from openlp.core.common import translate from openlp.core.common import translate
from openlp.core.common.path import Path
log = logging.getLogger(__name__ + '.__init__') log = logging.getLogger(__name__ + '.__init__')
@ -125,10 +126,11 @@ def build_icon(icon):
Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name. QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
:param icon: :param QtGui.QIcon | Path | QtGui.QIcon | str icon:
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file path
location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string. location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string.
:return: The build icon. :return: The build icon.
:rtype: QtGui.QIcon
""" """
if isinstance(icon, QtGui.QIcon): if isinstance(icon, QtGui.QIcon):
return icon return icon
@ -136,6 +138,8 @@ def build_icon(icon):
button_icon = QtGui.QIcon() button_icon = QtGui.QIcon()
if isinstance(icon, str): if isinstance(icon, str):
pix_map = QtGui.QPixmap(icon) pix_map = QtGui.QPixmap(icon)
elif isinstance(icon, Path):
pix_map = QtGui.QPixmap(str(icon))
elif isinstance(icon, QtGui.QImage): elif isinstance(icon, QtGui.QImage):
pix_map = QtGui.QPixmap.fromImage(icon) pix_map = QtGui.QPixmap.fromImage(icon)
if pix_map: if pix_map:
@ -217,14 +221,15 @@ def validate_thumb(file_path, thumb_path):
Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function, Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
before checking the existence of the file. before checking the existence of the file.
:param file_path: The path to the file. The file **must** exist! :param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist!
:param thumb_path: The path to the thumb. :param openlp.core.common.path.Path thumb_path: The path to the thumb.
:return: True, False if the image has changed since the thumb was created. :return: Has the image changed since the thumb was created?
:rtype: bool
""" """
if not os.path.exists(thumb_path): if not thumb_path.exists():
return False return False
image_date = os.stat(file_path).st_mtime image_date = file_path.stat().st_mtime
thumb_date = os.stat(thumb_path).st_mtime thumb_date = thumb_path.stat().st_mtime
return image_date <= thumb_date return image_date <= thumb_date
@ -606,35 +611,6 @@ def create_separated_list(string_list):
return list_to_string return list_to_string
def replace_params(args, kwargs, params):
"""
Apply a transformation function to the specified args or kwargs
:param tuple args: Positional arguments
:param dict kwargs: Key Word arguments
:param params: A tuple of tuples with the position and the key word to replace.
:return: The modified positional and keyword arguments
:rtype: tuple[tuple, dict]
Usage:
Take a method with the following signature, and assume we which to apply the str function to arg2:
def method(arg1=None, arg2=None, arg3=None)
As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
would call this function as follows:
replace_params(args, kwargs, ((1, 'arg2', str),))
"""
args = list(args)
for position, key_word, transform in params:
if len(args) > position:
args[position] = transform(args[position])
elif key_word in kwargs:
kwargs[key_word] = transform(kwargs[key_word])
return tuple(args), kwargs
from .exceptions import ValidationError from .exceptions import ValidationError
from .screen import ScreenList from .screen import ScreenList
from .formattingtags import FormattingTags from .formattingtags import FormattingTags

View File

@ -23,12 +23,13 @@
""" """
The :mod:`db` module provides the core database functionality for OpenLP The :mod:`db` module provides the core database functionality for OpenLP
""" """
import json
import logging import logging
import os import os
from copy import copy from copy import copy
from urllib.parse import quote_plus as urlquote from urllib.parse import quote_plus as urlquote
from sqlalchemy import Table, MetaData, Column, types, create_engine from sqlalchemy import Table, MetaData, Column, types, create_engine, UnicodeText
from sqlalchemy.engine.url import make_url from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError
from sqlalchemy.orm import scoped_session, sessionmaker, mapper from sqlalchemy.orm import scoped_session, sessionmaker, mapper
@ -37,7 +38,8 @@ from sqlalchemy.pool import NullPool
from alembic.migration import MigrationContext from alembic.migration import MigrationContext
from alembic.operations import Operations from alembic.operations import Operations
from openlp.core.common import AppLocation, Settings, translate, delete_file from openlp.core.common import AppLocation, Settings, delete_file, translate
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -133,9 +135,10 @@ def get_db_path(plugin_name, db_file_name=None):
if db_file_name is None: if db_file_name is None:
return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name), return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name),
plugin=plugin_name) plugin=plugin_name)
elif os.path.isabs(db_file_name):
return 'sqlite:///{db_file_name}'.format(db_file_name=db_file_name)
else: else:
return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), name=db_file_name)
name=db_file_name)
def handle_db_error(plugin_name, db_file_name): def handle_db_error(plugin_name, db_file_name):
@ -200,6 +203,55 @@ class BaseModel(object):
return instance return instance
class PathType(types.TypeDecorator):
"""
Create a PathType for storing Path objects with SQLAlchemy. Behind the scenes we convert the Path object to a JSON
representation and store it as a Unicode type
"""
impl = types.UnicodeText
def coerce_compared_value(self, op, value):
"""
Some times it make sense to compare a PathType with a string. In the case a string is used coerce the the
PathType to a UnicodeText type.
:param op: The operation being carried out. Not used, as we only care about the type that is being used with the
operation.
:param openlp.core.common.path.Path | str value: The value being used for the comparison. Most likely a Path
Object or str.
:return: The coerced value stored in the db
:rtype: PathType or UnicodeText
"""
if isinstance(value, str):
return UnicodeText()
else:
return self
def process_bind_param(self, value, dialect):
"""
Convert the Path object to a JSON representation
:param openlp.core.common.path.Path value: The value to convert
:param dialect: Not used
:return: The Path object as a JSON string
:rtype: str
"""
data_path = AppLocation.get_data_path()
return json.dumps(value, cls=OpenLPJsonEncoder, base_path=data_path)
def process_result_value(self, value, dialect):
"""
Convert the JSON representation back
:param types.UnicodeText value: The value to convert
:param dialect: Not used
:return: The JSON object converted Python object (in this case it should be a Path object)
:rtype: openlp.core.common.path.Path
"""
data_path = AppLocation.get_data_path()
return json.loads(value, cls=OpenLPJsonDecoder, base_path=data_path)
def upgrade_db(url, upgrade): def upgrade_db(url, upgrade):
""" """
Upgrade a database. Upgrade a database.
@ -208,7 +260,7 @@ def upgrade_db(url, upgrade):
:param upgrade: The python module that contains the upgrade instructions. :param upgrade: The python module that contains the upgrade instructions.
""" """
if not database_exists(url): if not database_exists(url):
log.warn("Database {db} doesn't exist - skipping upgrade checks".format(db=url)) log.warning("Database {db} doesn't exist - skipping upgrade checks".format(db=url))
return (0, 0) return (0, 0)
log.debug('Checking upgrades for DB {db}'.format(db=url)) log.debug('Checking upgrades for DB {db}'.format(db=url))
@ -273,10 +325,11 @@ def delete_database(plugin_name, db_file_name=None):
:param plugin_name: The name of the plugin to remove the database for :param plugin_name: The name of the plugin to remove the database for
:param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used. :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
""" """
db_file_path = AppLocation.get_section_data_path(plugin_name)
if db_file_name: if db_file_name:
db_file_path = AppLocation.get_section_data_path(plugin_name) / db_file_name db_file_path = db_file_path / db_file_name
else: else:
db_file_path = AppLocation.get_section_data_path(plugin_name) / plugin_name db_file_path = db_file_path / plugin_name
return delete_file(db_file_path) return delete_file(db_file_path)
@ -284,30 +337,30 @@ class Manager(object):
""" """
Provide generic object persistence management Provide generic object persistence management
""" """
def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None, session=None): def __init__(self, plugin_name, init_schema, db_file_path=None, upgrade_mod=None, session=None):
""" """
Runs the initialisation process that includes creating the connection to the database and the tables if they do Runs the initialisation process that includes creating the connection to the database and the tables if they do
not exist. 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 init_schema: The init_schema function for this database
:param db_file_name: The upgrade_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
:param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name resulting in the plugin_name being used.
being used. :param upgrade_mod: The upgrade_schema function for this database
""" """
self.is_dirty = False self.is_dirty = False
self.session = None self.session = None
self.db_url = None self.db_url = None
if db_file_name: if db_file_path:
log.debug('Manager: Creating new DB url') log.debug('Manager: Creating new DB url')
self.db_url = init_url(plugin_name, db_file_name) self.db_url = init_url(plugin_name, str(db_file_path))
else: else:
self.db_url = init_url(plugin_name) self.db_url = init_url(plugin_name)
if upgrade_mod: if upgrade_mod:
try: try:
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
except (SQLAlchemyError, DBAPIError): except (SQLAlchemyError, DBAPIError):
handle_db_error(plugin_name, db_file_name) handle_db_error(plugin_name, str(db_file_path))
return return
if db_ver > up_ver: if db_ver > up_ver:
critical_error_message_box( critical_error_message_box(
@ -322,7 +375,7 @@ class Manager(object):
try: try:
self.session = init_schema(self.db_url) self.session = init_schema(self.db_url)
except (SQLAlchemyError, DBAPIError): except (SQLAlchemyError, DBAPIError):
handle_db_error(plugin_name, db_file_name) handle_db_error(plugin_name, str(db_file_path))
else: else:
self.session = session self.session = session

View File

@ -4,7 +4,7 @@
"color": "#000000", "color": "#000000",
"direction": "vertical", "direction": "vertical",
"end_color": "#000000", "end_color": "#000000",
"filename": "", "filename": null,
"start_color": "#000000", "start_color": "#000000",
"type": "solid" "type": "solid"
}, },

View File

@ -359,10 +359,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
:param files: The files to be loaded. :param files: The files to be loaded.
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
""" """
names = []
full_list = [] full_list = []
for count in range(self.list_view.count()): for count in range(self.list_view.count()):
names.append(self.list_view.item(count).text())
full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole)) full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
duplicates_found = False duplicates_found = False
files_added = False files_added = False

View File

@ -27,7 +27,7 @@ import logging
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
from openlp.core.common.versionchecker import get_application_version from openlp.core.version import get_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -139,7 +139,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
if version: if version:
self.version = version self.version = version
else: else:
self.version = get_application_version()['version'] self.version = get_version()['version']
self.settings_section = self.name self.settings_section = self.name
self.icon = None self.icon = None
self.media_item_class = media_item_class self.media_item_class = media_item_class

View File

@ -341,9 +341,9 @@ class ProjectorDB(Manager):
""" """
old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip) old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip)
if old_projector is not None: if old_projector is not None:
log.warning('add_new() skipping entry ip="{ip}" (Already saved)'.format(ip=old_projector.ip)) log.warning('add_projector() skipping entry ip="{ip}" (Already saved)'.format(ip=old_projector.ip))
return False return False
log.debug('add_new() saving new entry') log.debug('add_projector() saving new entry')
log.debug('ip="{ip}", name="{name}", location="{location}"'.format(ip=projector.ip, log.debug('ip="{ip}", name="{name}", location="{location}"'.format(ip=projector.ip,
name=projector.name, name=projector.name,
location=projector.location)) location=projector.location))

View File

@ -72,6 +72,28 @@ PJLINK_HEADER = '{prefix}{{linkclass}}'.format(prefix=PJLINK_PREFIX)
PJLINK_SUFFIX = CR PJLINK_SUFFIX = CR
class PJLinkUDP(QtNetwork.QUdpSocket):
"""
Socket service for PJLink UDP socket.
"""
# New commands available in PJLink Class 2
pjlink_udp_commands = [
'ACKN', # Class 2 (cmd is SRCH)
'ERST', # Class 1/2
'INPT', # Class 1/2
'LKUP', # Class 2 (reply only - no cmd)
'POWR', # Class 1/2
'SRCH' # Class 2 (reply is ACKN)
]
def __init__(self, port=PJLINK_PORT):
"""
Initialize socket
"""
self.port = port
class PJLinkCommands(object): class PJLinkCommands(object):
""" """
Process replies from PJLink projector. Process replies from PJLink projector.
@ -488,7 +510,7 @@ class PJLinkCommands(object):
class PJLink(PJLinkCommands, QtNetwork.QTcpSocket): class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
""" """
Socket service for connecting to a PJLink-capable projector. Socket service for PJLink TCP socket.
""" """
# Signals sent by this module # Signals sent by this module
changeStatus = QtCore.pyqtSignal(str, int, str) changeStatus = QtCore.pyqtSignal(str, int, str)
@ -499,43 +521,29 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
# New commands available in PJLink Class 2 def __init__(self, projector, *args, **kwargs):
pjlink_udp_commands = [
'ACKN', # Class 2
'ERST', # Class 1 or 2
'INPT', # Class 1 or 2
'LKUP', # Class 2
'POWR', # Class 1 or 2
'SRCH' # Class 2
]
def __init__(self, port=PJLINK_PORT, *args, **kwargs):
""" """
Setup for instance. Setup for instance.
Options should be in kwargs except for port which does have a default. Options should be in kwargs except for port which does have a default.
:param name: Display name :param projector: Database record of projector
:param ip: IP address to connect to
:param port: Port to use. Default to PJLINK_PORT
:param pin: Access pin (if needed)
Optional parameters Optional parameters
:param dbid: Database ID number
:param location: Location where projector is physically located
:param notes: Extra notes about the projector
:param poll_time: Time (in seconds) to poll connected projector :param poll_time: Time (in seconds) to poll connected projector
:param socket_timeout: Time (in seconds) to abort the connection if no response :param socket_timeout: Time (in seconds) to abort the connection if no response
""" """
log.debug('PJlink(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs)) log.debug('PJlink(projector={projector}, args={args} kwargs={kwargs})'.format(projector=projector,
args=args,
kwargs=kwargs))
super().__init__() super().__init__()
self.dbid = kwargs.get('dbid') self.entry = projector
self.ip = kwargs.get('ip') self.ip = self.entry.ip
self.location = kwargs.get('location') self.location = self.entry.location
self.mac_adx = kwargs.get('mac_adx') self.mac_adx = self.entry.mac_adx
self.name = kwargs.get('name') self.name = self.entry.name
self.notes = kwargs.get('notes') self.notes = self.entry.notes
self.pin = kwargs.get('pin') self.pin = self.entry.pin
self.port = port self.port = self.entry.port
self.db_update = False # Use to check if db needs to be updated prior to exiting self.db_update = False # Use to check if db needs to be updated prior to exiting
# Poll time 20 seconds unless called with something else # Poll time 20 seconds unless called with something else
self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000 self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
@ -751,7 +759,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.change_status(E_AUTHENTICATION) self.change_status(E_AUTHENTICATION)
log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip)) log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip))
return return
elif data_check[1] == '0' and self.pin is not None: elif (data_check[1] == '0') and (self.pin):
# Pin set and no authentication needed # Pin set and no authentication needed
log.warning('({ip}) Regular connection but PIN set'.format(ip=self.name)) log.warning('({ip}) Regular connection but PIN set'.format(ip=self.name))
self.disconnect_from_host() self.disconnect_from_host()
@ -761,7 +769,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
return return
elif data_check[1] == '1': elif data_check[1] == '1':
# Authenticated login with salt # Authenticated login with salt
if self.pin is None: if not self.pin:
log.warning('({ip}) Authenticated connection but no pin set'.format(ip=self.ip)) log.warning('({ip}) Authenticated connection but no pin set'.format(ip=self.ip))
self.disconnect_from_host() self.disconnect_from_host()
self.change_status(E_AUTHENTICATION) self.change_status(E_AUTHENTICATION)
@ -776,7 +784,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
else: else:
data_hash = None data_hash = None
# We're connected at this point, so go ahead and setup regular I/O # We're connected at this point, so go ahead and setup regular I/O
self.readyRead.connect(self.get_data) self.readyRead.connect(self.get_socket)
self.projectorReceivedData.connect(self._send_command) self.projectorReceivedData.connect(self._send_command)
# Initial data we should know about # Initial data we should know about
self.send_command(cmd='CLSS', salt=data_hash) self.send_command(cmd='CLSS', salt=data_hash)
@ -800,27 +808,51 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
count=trash_count)) count=trash_count))
return return
@QtCore.pyqtSlot(str, str)
def get_buffer(self, data, ip):
"""
Get data from somewhere other than TCP socket
:param data: Data to process. buffer must be formatted as a proper PJLink packet.
:param ip: Destination IP for buffer.
"""
log.debug("({ip}) get_buffer(data='{buff}' ip='{ip_in}'".format(ip=self.ip, buff=data, ip_in=ip))
if ip is None:
log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.ip))
return
return self.get_data(buff=data, ip=ip)
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def get_data(self): def get_socket(self):
""" """
Socket interface to retrieve data. Get data from TCP socket.
""" """
log.debug('({ip}) get_data(): Reading data'.format(ip=self.ip)) log.debug('({ip}) get_socket(): Reading data'.format(ip=self.ip))
if self.state() != self.ConnectedState: if self.state() != self.ConnectedState:
log.debug('({ip}) get_data(): Not connected - returning'.format(ip=self.ip)) log.debug('({ip}) get_socket(): Not connected - returning'.format(ip=self.ip))
self.send_busy = False self.send_busy = False
return return
# Although we have a packet length limit, go ahead and use a larger buffer # Although we have a packet length limit, go ahead and use a larger buffer
read = self.readLine(1024) read = self.readLine(1024)
log.debug("({ip}) get_data(): '{buff}'".format(ip=self.ip, buff=read)) log.debug("({ip}) get_socket(): '{buff}'".format(ip=self.ip, buff=read))
if read == -1: if read == -1:
# No data available # No data available
log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip)) log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.ip))
return self.receive_data_signal() return self.receive_data_signal()
self.socket_timer.stop() self.socket_timer.stop()
self.projectorNetwork.emit(S_NETWORK_RECEIVED) self.projectorNetwork.emit(S_NETWORK_RECEIVED)
return self.get_data(buff=read, ip=self.ip)
def get_data(self, buff, ip):
"""
Process received data
:param buff: Data to process.
:param ip: (optional) Destination IP.
"""
log.debug("({ip}) get_data(ip='{ip_in}' buffer='{buff}'".format(ip=self.ip, ip_in=ip, buff=buff))
# NOTE: Class2 has changed to some values being UTF-8 # NOTE: Class2 has changed to some values being UTF-8
data_in = decode(read, 'utf-8') data_in = decode(buff, 'utf-8')
data = data_in.strip() data = data_in.strip()
if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)): if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)):
return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix') return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix')
@ -990,7 +1022,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
self.reset_information() self.reset_information()
self.disconnectFromHost() self.disconnectFromHost()
try: try:
self.readyRead.disconnect(self.get_data) self.readyRead.disconnect(self.get_socket)
except TypeError: except TypeError:
pass pass
if abort: if abort:

View File

@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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 #
###############################################################################
"""
:mod:`openlp.core.lib.projector.pjlink2` module provides the PJLink Class 2
updates from PJLink Class 1.
This module only handles the UDP socket functionality. Command/query/status
change messages will still be processed by the PJLink 1 module.
Currently, the only variance is the addition of a UDP "search" command to
query the local network for Class 2 capable projectors,
and UDP "notify" messages from projectors to connected software of status
changes (i.e., power change, input change, error changes).
Differences between Class 1 and Class 2 PJLink specifications are as follows.
New Functionality:
* Search - UDP Query local network for Class 2 capabable projector(s).
* Status - UDP Status change with connected projector(s). Status change
messages consist of:
* Initial projector power up when network communication becomes available
* Lamp off/standby to warmup or on
* Lamp on to cooldown or off/standby
* Input source select change completed
* Error status change (i.e., fan/lamp/temp/cover open/filter/other error(s))
New Commands:
* Query serial number of projector
* Query version number of projector software
* Query model number of replacement lamp
* Query model number of replacement air filter
* Query current projector screen resolution
* Query recommended screen resolution
* Query name of specific input terminal (video source)
* Adjust projector microphone in 1-step increments
* Adjust projector speacker in 1-step increments
Extended Commands:
* Addition of INTERNAL terminal (video source) for a total of 6 types of terminals.
* Number of terminals (video source) has been expanded from [1-9]
to [1-9a-z] (Addition of 26 terminals for each type of input).
See PJLink Class 2 Specifications for details.
http://pjlink.jbmia.or.jp/english/dl_class2.html
Section 5-1 PJLink Specifications
Section 5-5 Guidelines for Input Terminals
"""
import logging
log = logging.getLogger(__name__)
log.debug('pjlink2 loaded')
from PyQt5 import QtNetwork
class PJLinkUDP(QtNetwork.QUdpSocket):
"""
Socket service for handling datagram (UDP) sockets.
"""
log.debug('PJLinkUDP loaded')
# Class varialbe for projector list. Should be replaced by ProjectorManager's
# projector list after being loaded there.
projector_list = None
projectors_found = None # UDP search found list

View File

@ -26,6 +26,7 @@ from string import Template
from PyQt5 import QtGui, QtCore, QtWebKitWidgets from PyQt5 import QtGui, QtCore, QtWebKitWidgets
from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
from openlp.core.common.path import path_to_str
from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \ from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
from openlp.core.common import ThemeLevel from openlp.core.common import ThemeLevel
@ -118,7 +119,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name] theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
# if No file do not update cache # if No file do not update cache
if theme_data.background_filename: if theme_data.background_filename:
self.image_manager.add_image(theme_data.background_filename, self.image_manager.add_image(path_to_str(theme_data.background_filename),
ImageSource.Theme, QtGui.QColor(theme_data.background_border_color)) ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
def pre_render(self, override_theme_data=None): def pre_render(self, override_theme_data=None):
@ -207,8 +208,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
service_item.raw_footer = FOOTER service_item.raw_footer = FOOTER
# if No file do not update cache # if No file do not update cache
if theme_data.background_filename: if theme_data.background_filename:
self.image_manager.add_image( self.image_manager.add_image(path_to_str(theme_data.background_filename),
theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color)) ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
theme_data, main, footer = self.pre_render(theme_data) theme_data, main, footer = self.pre_render(theme_data)
service_item.theme_data = theme_data service_item.theme_data = theme_data
service_item.main = main service_item.main = main

View File

@ -22,13 +22,13 @@
""" """
Provide the theme XML and handling functions for OpenLP v2 themes. Provide the theme XML and handling functions for OpenLP v2 themes.
""" """
import os
import logging
import json import json
import logging
from lxml import etree, objectify from lxml import etree, objectify
from openlp.core.common import AppLocation, de_hump from openlp.core.common import AppLocation, de_hump
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
from openlp.core.common.path import Path, str_to_path
from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -160,9 +160,8 @@ class Theme(object):
# basic theme object with defaults # basic theme object with defaults
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json' json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
jsn = get_text_file_string(json_path) jsn = get_text_file_string(json_path)
jsn = json.loads(jsn) self.load_theme(jsn)
self.expand_json(jsn) self.background_filename = None
self.background_filename = ''
def expand_json(self, var, prev=None): def expand_json(self, var, prev=None):
""" """
@ -174,8 +173,6 @@ class Theme(object):
for key, value in var.items(): for key, value in var.items():
if prev: if prev:
key = prev + "_" + key key = prev + "_" + key
else:
key = key
if isinstance(value, dict): if isinstance(value, dict):
self.expand_json(value, key) self.expand_json(value, key)
else: else:
@ -185,13 +182,13 @@ class Theme(object):
""" """
Add the path name to the image name so the background can be rendered. Add the path name to the image name so the background can be rendered.
:param path: The path name to be added. :param openlp.core.common.path.Path path: The path name to be added.
:rtype: None
""" """
if self.background_type == 'image' or self.background_type == 'video': if self.background_type == 'image' or self.background_type == 'video':
if self.background_filename and path: if self.background_filename and path:
self.theme_name = self.theme_name.strip() self.theme_name = self.theme_name.strip()
self.background_filename = self.background_filename.strip() self.background_filename = path / self.theme_name / self.background_filename
self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
def set_default_header_footer(self): def set_default_header_footer(self):
""" """
@ -206,16 +203,21 @@ class Theme(object):
self.font_footer_y = current_screen['size'].height() * 9 / 10 self.font_footer_y = current_screen['size'].height() * 9 / 10
self.font_footer_height = current_screen['size'].height() / 10 self.font_footer_height = current_screen['size'].height() / 10
def load_theme(self, theme): def load_theme(self, theme, theme_path=None):
""" """
Convert the JSON file and expand it. Convert the JSON file and expand it.
:param theme: the theme string :param theme: the theme string
:param openlp.core.common.path.Path theme_path: The path to the theme
:rtype: None
""" """
jsn = json.loads(theme) if theme_path:
jsn = json.loads(theme, cls=OpenLPJsonDecoder, base_path=theme_path)
else:
jsn = json.loads(theme, cls=OpenLPJsonDecoder)
self.expand_json(jsn) self.expand_json(jsn)
def export_theme(self): def export_theme(self, theme_path=None):
""" """
Loop through the fields and build a dictionary of them Loop through the fields and build a dictionary of them
@ -223,7 +225,9 @@ class Theme(object):
theme_data = {} theme_data = {}
for attr, value in self.__dict__.items(): for attr, value in self.__dict__.items():
theme_data["{attr}".format(attr=attr)] = value theme_data["{attr}".format(attr=attr)] = value
return json.dumps(theme_data) if theme_path:
return json.dumps(theme_data, cls=OpenLPJsonEncoder, base_path=theme_path)
return json.dumps(theme_data, cls=OpenLPJsonEncoder)
def parse(self, xml): def parse(self, xml):
""" """

View File

@ -20,45 +20,36 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
Package to test the openlp.core.common.versionchecker package. The :mod:`openlp.core.threading` module contains some common threading code
""" """
from unittest import TestCase from PyQt5 import QtCore
from unittest.mock import MagicMock, patch
from openlp.core.common.settings import Settings
from openlp.core.common.versionchecker import VersionThread
from tests.helpers.testmixin import TestMixin
class TestVersionchecker(TestMixin, TestCase): def run_thread(parent, worker, prefix='', auto_start=True):
"""
Create a thread and assign a worker to it. This removes a lot of boilerplate code from the codebase.
def setUp(self): :param object parent: The parent object so that the thread and worker are not orphaned.
""" :param QObject worker: A QObject-based worker object which does the actual work.
Create an instance and a few example actions. :param str prefix: A prefix to be applied to the attribute names.
""" :param bool auto_start: Automatically start the thread. Defaults to True.
self.build_settings() """
# Set up attribute names
def tearDown(self): thread_name = 'thread'
""" worker_name = 'worker'
Clean up if prefix:
""" thread_name = '_'.join([prefix, thread_name])
self.destroy_settings() worker_name = '_'.join([prefix, worker_name])
# Create the thread and add the thread and the worker to the parent
def test_version_thread_triggered(self): thread = QtCore.QThread()
""" setattr(parent, thread_name, thread)
Test the version thread call does not trigger UI setattr(parent, worker_name, worker)
:return: # Move the worker into the thread's context
""" worker.moveToThread(thread)
# GIVEN: a equal version setup and the data is not today. # Connect slots and signals
mocked_main_window = MagicMock() thread.started.connect(worker.start)
Settings().setValue('core/last version test', '1950-04-01') worker.quit.connect(thread.quit)
# WHEN: We check to see if the version is different . worker.quit.connect(worker.deleteLater)
with patch('PyQt5.QtCore.QThread'),\ thread.finished.connect(thread.deleteLater)
patch('openlp.core.common.versionchecker.get_application_version') as mocked_get_application_version: if auto_start:
mocked_get_application_version.return_value = {'version': '1.0.0', 'build': '', 'full': '2.0.4'} thread.start()
version_thread = VersionThread(mocked_main_window)
version_thread.run()
# THEN: If the version has changed the main window is notified
self.assertTrue(mocked_main_window.openlp_version_check.emit.called,
'The main windows should have been notified')

View File

@ -26,7 +26,7 @@ import webbrowser
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common.versionchecker import get_application_version from openlp.core.version import get_version
from openlp.core.lib import translate from openlp.core.lib import translate
from .aboutdialog import UiAboutDialog from .aboutdialog import UiAboutDialog
@ -49,7 +49,7 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
Set up the dialog. This method is mocked out in tests. Set up the dialog. This method is mocked out in tests.
""" """
self.setup_ui(self) self.setup_ui(self)
application_version = get_application_version() application_version = get_version()
about_text = self.about_text_edit.toPlainText() about_text = self.about_text_edit.toPlainText()
about_text = about_text.replace('<version>', application_version['version']) about_text = about_text.replace('<version>', application_version['version'])
if application_version['build']: if application_version['build']:

View File

@ -22,9 +22,8 @@
""" """
The :mod:`advancedtab` provides an advanced settings facility. The :mod:`advancedtab` provides an advanced settings facility.
""" """
from datetime import datetime, timedelta
import logging import logging
import os from datetime import datetime, timedelta
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
@ -492,24 +491,27 @@ class AdvancedTab(SettingsTab):
self.service_name_edit.setText(UiStrings().DefaultServiceName) self.service_name_edit.setText(UiStrings().DefaultServiceName)
self.service_name_edit.setFocus() self.service_name_edit.setFocus()
def on_data_directory_path_edit_path_changed(self, new_data_path): def on_data_directory_path_edit_path_changed(self, new_path):
""" """
Browse for a new data directory location. Handle the `editPathChanged` signal of the data_directory_path_edit
:param openlp.core.common.path.Path new_path: The new path
:rtype: None
""" """
# Make sure they want to change the data. # Make sure they want to change the data.
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'), answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the ' translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
'location of the OpenLP data directory to:\n\n{path}' 'location of the OpenLP data directory to:\n\n{path}'
'\n\nThe data directory will be changed when OpenLP is ' '\n\nThe data directory will be changed when OpenLP is '
'closed.').format(path=new_data_path), 'closed.').format(path=new_path),
defaultButton=QtWidgets.QMessageBox.No) defaultButton=QtWidgets.QMessageBox.No)
if answer != QtWidgets.QMessageBox.Yes: if answer != QtWidgets.QMessageBox.Yes:
self.data_directory_path_edit.path = AppLocation.get_data_path() self.data_directory_path_edit.path = AppLocation.get_data_path()
return return
# Check if data already exists here. # Check if data already exists here.
self.check_data_overwrite(path_to_str(new_data_path)) self.check_data_overwrite(new_path)
# Save the new location. # Save the new location.
self.main_window.set_new_data_path(path_to_str(new_data_path)) self.main_window.new_data_path = new_path
self.data_directory_cancel_button.show() self.data_directory_cancel_button.show()
def on_data_directory_copy_check_box_toggled(self): def on_data_directory_copy_check_box_toggled(self):
@ -526,9 +528,10 @@ class AdvancedTab(SettingsTab):
def check_data_overwrite(self, data_path): def check_data_overwrite(self, data_path):
""" """
Check if there's already data in the target directory. Check if there's already data in the target directory.
:param openlp.core.common.path.Path data_path: The target directory to check
""" """
test_path = os.path.join(data_path, 'songs') if (data_path / 'songs').exists():
if os.path.exists(test_path):
self.data_exists = True self.data_exists = True
# Check is they want to replace existing data. # Check is they want to replace existing data.
answer = QtWidgets.QMessageBox.warning(self, answer = QtWidgets.QMessageBox.warning(self,
@ -537,7 +540,7 @@ class AdvancedTab(SettingsTab):
'WARNING: \n\nThe location you have selected \n\n{path}' 'WARNING: \n\nThe location you have selected \n\n{path}'
'\n\nappears to contain OpenLP data files. Do you wish to ' '\n\nappears to contain OpenLP data files. Do you wish to '
'replace these files with the current data ' 'replace these files with the current data '
'files?').format(path=os.path.abspath(data_path,)), 'files?'.format(path=data_path)),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No), QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No) QtWidgets.QMessageBox.No)
@ -559,7 +562,7 @@ class AdvancedTab(SettingsTab):
""" """
self.data_directory_path_edit.path = AppLocation.get_data_path() self.data_directory_path_edit.path = AppLocation.get_data_path()
self.data_directory_copy_check_box.setChecked(False) self.data_directory_copy_check_box.setChecked(False)
self.main_window.set_new_data_path(None) self.main_window.new_data_path = None
self.main_window.set_copy_data(False) self.main_window.set_copy_data(False)
self.data_directory_copy_check_box.hide() self.data_directory_copy_check_box.hide()
self.data_directory_cancel_button.hide() self.data_directory_cancel_button.hide()

View File

@ -71,7 +71,7 @@ except ImportError:
VLC_VERSION = '-' VLC_VERSION = '-'
from openlp.core.common import RegistryProperties, Settings, UiStrings, is_linux, translate from openlp.core.common import RegistryProperties, Settings, UiStrings, is_linux, translate
from openlp.core.common.versionchecker import get_application_version from openlp.core.version import get_version
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.ui.lib.filedialog import FileDialog
from .exceptiondialog import Ui_ExceptionDialog from .exceptiondialog import Ui_ExceptionDialog
@ -110,7 +110,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
""" """
Create an exception report. Create an exception report.
""" """
openlp_version = get_application_version() openlp_version = get_version()
description = self.description_text_edit.toPlainText() description = self.description_text_edit.toPlainText()
traceback = self.exception_text_edit.toPlainText() traceback = self.exception_text_edit.toPlainText()
system = translate('OpenLP.ExceptionForm', 'Platform: {platform}\n').format(platform=platform.platform()) system = translate('OpenLP.ExceptionForm', 'Platform: {platform}\n').format(platform=platform.platform())
@ -149,21 +149,11 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
opts = self._create_report() opts = self._create_report()
report_text = self.report_text.format(version=opts['version'], description=opts['description'], report_text = self.report_text.format(version=opts['version'], description=opts['description'],
traceback=opts['traceback'], libs=opts['libs'], system=opts['system']) traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
filename = str(file_path)
try: try:
report_file = open(filename, 'w') with file_path.open('w') as report_file:
try:
report_file.write(report_text) report_file.write(report_text)
except UnicodeError:
report_file.close()
report_file = open(filename, 'wb')
report_file.write(report_text.encode('utf-8'))
finally:
report_file.close()
except IOError: except IOError:
log.exception('Failed to write crash report') log.exception('Failed to write crash report')
finally:
report_file.close()
def on_send_report_button_clicked(self): def on_send_report_button_clicked(self):
""" """
@ -219,7 +209,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
translate('ImagePlugin.ExceptionDialog', 'Select Attachment'), translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
Settings().value(self.settings_section + '/last directory'), Settings().value(self.settings_section + '/last directory'),
'{text} (*)'.format(text=UiStrings().AllFiles)) '{text} (*)'.format(text=UiStrings().AllFiles))
log.info('New file {file}'.format(file=file_path)) log.info('New files {file_path}'.format(file_path=file_path))
if file_path: if file_path:
self.file_attachment = str(file_path) self.file_attachment = str(file_path)

View File

@ -181,22 +181,16 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self.application.process_events() self.application.process_events()
try: 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.cfg'),
header=('User-Agent', user_agent)) headers={'User-Agent': user_agent})
except (urllib.error.URLError, ConnectionError) as err: except ConnectionError:
msg = QtWidgets.QMessageBox() QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),
title = translate('OpenLP.FirstTimeWizard', 'Network Error') translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '
msg.setText('{title} {error}'.format(title=title, 'to connect to retrieve initial configuration information'),
error=err.code if hasattr(err, 'code') else '')) QtWidgets.QMessageBox.Ok)
msg.setInformativeText(translate('OpenLP.FirstTimeWizard',
'There was a network error attempting to '
'connect to retrieve initial configuration information'))
msg.setStandardButtons(msg.Ok)
ans = msg.exec()
web_config = False web_config = False
if web_config: if web_config:
files = web_config.read()
try: try:
self.config.read_string(files.decode()) self.config.read_string(web_config)
self.web = self.config.get('general', 'base url') self.web = self.config.get('general', 'base url')
self.songs_url = self.web + self.config.get('songs', 'directory') + '/' self.songs_url = self.web + self.config.get('songs', 'directory') + '/'
self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/'
@ -563,7 +557,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
filename, sha256 = item.data(QtCore.Qt.UserRole) filename, sha256 = item.data(QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=filename), 0) self._increment_progress_bar(self.downloading.format(name=filename), 0)
self.previous_size = 0 self.previous_size = 0
destination = os.path.join(songs_destination, str(filename)) destination = Path(songs_destination, str(filename))
if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
destination, sha256): destination, sha256):
missed_files.append('Song: {name}'.format(name=filename)) missed_files.append('Song: {name}'.format(name=filename))
@ -576,7 +570,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self._increment_progress_bar(self.downloading.format(name=bible), 0) self._increment_progress_bar(self.downloading.format(name=bible), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
os.path.join(bibles_destination, bible), Path(bibles_destination, bible),
sha256): sha256):
missed_files.append('Bible: {name}'.format(name=bible)) missed_files.append('Bible: {name}'.format(name=bible))
bibles_iterator += 1 bibles_iterator += 1
@ -588,7 +582,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
self._increment_progress_bar(self.downloading.format(name=theme), 0) self._increment_progress_bar(self.downloading.format(name=theme), 0)
self.previous_size = 0 self.previous_size = 0
if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme),
os.path.join(themes_destination, theme), Path(themes_destination, theme),
sha256): sha256):
missed_files.append('Theme: {name}'.format(name=theme)) missed_files.append('Theme: {name}'.format(name=theme))
if missed_files: if missed_files:

View File

@ -163,7 +163,6 @@ class GeneralTab(SettingsTab):
self.startup_layout.addWidget(self.show_splash_check_box) self.startup_layout.addWidget(self.show_splash_check_box)
self.check_for_updates_check_box = QtWidgets.QCheckBox(self.startup_group_box) self.check_for_updates_check_box = QtWidgets.QCheckBox(self.startup_group_box)
self.check_for_updates_check_box.setObjectName('check_for_updates_check_box') self.check_for_updates_check_box.setObjectName('check_for_updates_check_box')
self.check_for_updates_check_box.setVisible(False)
self.startup_layout.addWidget(self.check_for_updates_check_box) self.startup_layout.addWidget(self.check_for_updates_check_box)
self.right_layout.addWidget(self.startup_group_box) self.right_layout.addWidget(self.startup_group_box)
# Logo # Logo

View File

@ -22,8 +22,7 @@
""" Patch the QFileDialog so it accepts and returns Path objects""" """ Patch the QFileDialog so it accepts and returns Path objects"""
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.path import Path, path_to_str, replace_params, str_to_path
from openlp.core.lib import replace_params
class FileDialog(QtWidgets.QFileDialog): class FileDialog(QtWidgets.QFileDialog):
@ -32,11 +31,11 @@ class FileDialog(QtWidgets.QFileDialog):
""" """
Wraps `getExistingDirectory` so that it can be called with, and return Path objects Wraps `getExistingDirectory` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None :type parent: QtWidgets.QWidget | None
:type caption: str :type caption: str
:type directory: openlp.core.common.path.Path :type directory: openlp.core.common.path.Path
:type options: QtWidgets.QFileDialog.Options :type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path, str] :rtype: tuple[openlp.core.common.path.Path, str]
""" """
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
@ -51,13 +50,13 @@ class FileDialog(QtWidgets.QFileDialog):
""" """
Wraps `getOpenFileName` so that it can be called with, and return Path objects Wraps `getOpenFileName` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None :type parent: QtWidgets.QWidget | None
:type caption: str :type caption: str
:type directory: openlp.core.common.path.Path :type directory: openlp.core.common.path.Path
:type filter: str :type filter: str
:type initialFilter: str :type initialFilter: str
:type options: QtWidgets.QFileDialog.Options :type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path, str] :rtype: tuple[openlp.core.common.path.Path, str]
""" """
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
@ -72,13 +71,13 @@ class FileDialog(QtWidgets.QFileDialog):
""" """
Wraps `getOpenFileNames` so that it can be called with, and return Path objects Wraps `getOpenFileNames` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None :type parent: QtWidgets.QWidget | None
:type caption: str :type caption: str
:type directory: openlp.core.common.path.Path :type directory: openlp.core.common.path.Path
:type filter: str :type filter: str
:type initialFilter: str :type initialFilter: str
:type options: QtWidgets.QFileDialog.Options :type options: QtWidgets.QFileDialog.Options
:rtype: tuple[list[Path], str] :rtype: tuple[list[openlp.core.common.path.Path], str]
""" """
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
@ -94,13 +93,13 @@ class FileDialog(QtWidgets.QFileDialog):
""" """
Wraps `getSaveFileName` so that it can be called with, and return Path objects Wraps `getSaveFileName` so that it can be called with, and return Path objects
:type parent: QtWidgets.QWidget or None :type parent: QtWidgets.QWidget | None
:type caption: str :type caption: str
:type directory: openlp.core.common.path.Path :type directory: openlp.core.common.path.Path
:type filter: str :type filter: str
:type initialFilter: str :type initialFilter: str
:type options: QtWidgets.QFileDialog.Options :type options: QtWidgets.QFileDialog.Options
:rtype: tuple[Path or None, str] :rtype: tuple[openlp.core.common.path.Path | None, str]
""" """
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),)) args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))

View File

@ -310,7 +310,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
""" """
folder_path = FileDialog.getExistingDirectory( folder_path = FileDialog.getExistingDirectory(
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name), self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
QtWidgets.QFileDialog.ShowDirsOnly) FileDialog.ShowDirsOnly)
if folder_path: if folder_path:
editbox.setText(str(folder_path)) editbox.setText(str(folder_path))
Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path) Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)

View File

@ -346,7 +346,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
if not hasattr(self, 'service_item'): if not hasattr(self, 'service_item'):
return False return False
self.override['image'] = path self.override['image'] = path
self.override['theme'] = self.service_item.theme_data.background_filename self.override['theme'] = path_to_str(self.service_item.theme_data.background_filename)
self.image(path) self.image(path)
# Update the preview frame. # Update the preview frame.
if self.is_live: if self.is_live:
@ -454,7 +454,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
Registry().execute('video_background_replaced') Registry().execute('video_background_replaced')
self.override = {} self.override = {}
# We have a different theme. # We have a different theme.
elif self.override['theme'] != service_item.theme_data.background_filename: elif self.override['theme'] != path_to_str(service_item.theme_data.background_filename):
Registry().execute('live_theme_changed') Registry().execute('live_theme_changed')
self.override = {} self.override = {}
else: else:
@ -466,7 +466,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
if self.service_item.theme_data.background_type == 'image': if self.service_item.theme_data.background_type == 'image':
if self.service_item.theme_data.background_filename: if self.service_item.theme_data.background_filename:
self.service_item.bg_image_bytes = self.image_manager.get_image_bytes( self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(
self.service_item.theme_data.background_filename, ImageSource.Theme) path_to_str(self.service_item.theme_data.background_filename), ImageSource.Theme)
if image_path: if image_path:
image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin) image_bytes = self.image_manager.get_image_bytes(image_path, ImageSource.ImagePlugin)
created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes, created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
@ -488,7 +488,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
path = os.path.join(str(AppLocation.get_section_data_path('themes')), path = os.path.join(str(AppLocation.get_section_data_path('themes')),
self.service_item.theme_data.theme_name) self.service_item.theme_data.theme_name)
service_item.add_from_command(path, service_item.add_from_command(path,
self.service_item.theme_data.background_filename, path_to_str(self.service_item.theme_data.background_filename),
':/media/slidecontroller_multimedia.png') ':/media/slidecontroller_multimedia.png')
self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True) self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
self._hide_mouse() self._hide_mouse()

View File

@ -39,8 +39,7 @@ from openlp.core.api.http import server
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \ from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
check_directory_exists, translate, is_win, is_macosx, add_actions check_directory_exists, translate, is_win, is_macosx, add_actions
from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.common.actions import ActionList, CategoryOrder
from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.path import Path, copyfile, path_to_str, str_to_path
from openlp.core.common.versionchecker import get_application_version
from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
from openlp.core.lib.ui import create_action from openlp.core.lib.ui import create_action
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \ from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
@ -53,6 +52,7 @@ from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.ui.lib.mediadockmanager import MediaDockManager from openlp.core.ui.lib.mediadockmanager import MediaDockManager
from openlp.core.version import get_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -490,7 +490,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
The main window. The main window.
""" """
openlp_version_check = QtCore.pyqtSignal(QtCore.QVariant)
log.info('MainWindow loaded') log.info('MainWindow loaded')
def __init__(self): def __init__(self):
@ -499,6 +498,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
""" """
super(MainWindow, self).__init__() super(MainWindow, self).__init__()
Registry().register('main_window', self) Registry().register('main_window', self)
self.version_thread = None
self.version_worker = None
self.clipboard = self.application.clipboard() self.clipboard = self.application.clipboard()
self.arguments = ''.join(self.application.args) self.arguments = ''.join(self.application.args)
# Set up settings sections for the main application (not for use by plugins). # Set up settings sections for the main application (not for use by plugins).
@ -564,7 +565,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.application.set_busy_cursor() self.application.set_busy_cursor()
# Simple message boxes # Simple message boxes
Registry().register_function('theme_update_global', self.default_theme_changed) Registry().register_function('theme_update_global', self.default_theme_changed)
self.openlp_version_check.connect(self.version_notice)
Registry().register_function('config_screen_changed', self.screen_changed) Registry().register_function('config_screen_changed', self.screen_changed)
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
# Reset the cursor # Reset the cursor
@ -609,7 +609,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if widget: if widget:
widget.on_focus() widget.on_focus()
def version_notice(self, version): def on_new_version(self, version):
""" """
Notifies the user that a newer version of OpenLP is available. Notifies the user that a newer version of OpenLP is available.
Triggered by delay thread and cannot display popup. Triggered by delay thread and cannot display popup.
@ -619,7 +619,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.debug('version_notice') log.debug('version_notice')
version_text = translate('OpenLP.MainWindow', 'Version {new} of OpenLP is now available for download (you are ' version_text = translate('OpenLP.MainWindow', 'Version {new} of OpenLP is now available for download (you are '
'currently running version {current}). \n\nYou can download the latest version from ' 'currently running version {current}). \n\nYou can download the latest version from '
'http://openlp.org/.').format(new=version, current=get_application_version()[u'full']) 'http://openlp.org/.').format(new=version, current=get_version()[u'full'])
QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text) QtWidgets.QMessageBox.question(self, translate('OpenLP.MainWindow', 'OpenLP Version Updated'), version_text)
def show(self): def show(self):
@ -850,12 +850,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
QtWidgets.QMessageBox.No) QtWidgets.QMessageBox.No)
if answer == QtWidgets.QMessageBox.No: if answer == QtWidgets.QMessageBox.No:
return return
import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName( import_file_path, filter_used = FileDialog.getOpenFileName(
self, self,
translate('OpenLP.MainWindow', 'Import settings'), translate('OpenLP.MainWindow', 'Import settings'),
'', None,
translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)')) translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
if not import_file_name: if import_file_path is None:
return return
setting_sections = [] setting_sections = []
# Add main sections. # Add main sections.
@ -873,12 +873,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
# Add plugin sections. # Add plugin sections.
setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins]) setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins])
# Copy the settings file to the tmp dir, because we do not want to change the original one. # Copy the settings file to the tmp dir, because we do not want to change the original one.
temp_directory = os.path.join(str(gettempdir()), 'openlp') temp_dir_path = Path(gettempdir(), 'openlp')
check_directory_exists(Path(temp_directory)) check_directory_exists(temp_dir_path)
temp_config = os.path.join(temp_directory, os.path.basename(import_file_name)) temp_config_path = temp_dir_path / import_file_path.name
shutil.copyfile(import_file_name, temp_config) copyfile(import_file_path, temp_config_path)
settings = Settings() settings = Settings()
import_settings = Settings(temp_config, Settings.IniFormat) import_settings = Settings(str(temp_config_path), Settings.IniFormat)
log.info('hook upgrade_plugin_settings') log.info('hook upgrade_plugin_settings')
self.plugin_manager.hook_upgrade_plugin_settings(import_settings) self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
@ -922,7 +922,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.setValue('{key}'.format(key=section_key), value) settings.setValue('{key}'.format(key=section_key), value)
now = datetime.now() now = datetime.now()
settings.beginGroup(self.header_section) settings.beginGroup(self.header_section)
settings.setValue('file_imported', import_file_name) settings.setValue('file_imported', import_file_path)
settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M")) settings.setValue('file_date_imported', now.strftime("%Y-%m-%d %H:%M"))
settings.endGroup() settings.endGroup()
settings.sync() settings.sync()
@ -1013,6 +1013,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if not self.application.is_event_loop_active: if not self.application.is_event_loop_active:
event.ignore() event.ignore()
return return
# Sometimes the version thread hasn't finished, let's wait for it
try:
if self.version_thread and self.version_thread.isRunning():
wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
wait_dialog.show()
retry = 0
while self.version_thread.isRunning() and retry < 50:
self.application.processEvents()
self.version_thread.wait(100)
retry += 1
if self.version_thread.isRunning():
self.version_thread.terminate()
wait_dialog.close()
except RuntimeError:
# Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
pass
# If we just did a settings import, close without saving changes. # If we just did a settings import, close without saving changes.
if self.settings_imported: if self.settings_imported:
self.clean_up(False) self.clean_up(False)
@ -1334,12 +1353,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
if self.application: if self.application:
self.application.process_events() self.application.process_events()
def set_new_data_path(self, new_data_path):
"""
Set the new data path
"""
self.new_data_path = new_data_path
def set_copy_data(self, copy_data): def set_copy_data(self, copy_data):
""" """
Set the flag to copy the data Set the flag to copy the data
@ -1351,7 +1364,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
Change the data directory. Change the data directory.
""" """
log.info('Changing data path to {newpath}'.format(newpath=self.new_data_path)) log.info('Changing data path to {newpath}'.format(newpath=self.new_data_path))
old_data_path = str(AppLocation.get_data_path()) old_data_path = AppLocation.get_data_path()
# Copy OpenLP data to new location if requested. # Copy OpenLP data to new location if requested.
self.application.set_busy_cursor() self.application.set_busy_cursor()
if self.copy_data: if self.copy_data:
@ -1360,7 +1373,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
self.show_status_message( self.show_status_message(
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} ' translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
'- Please wait for copy to finish').format(path=self.new_data_path)) '- Please wait for copy to finish').format(path=self.new_data_path))
dir_util.copy_tree(old_data_path, self.new_data_path) dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
log.info('Copy successful') log.info('Copy successful')
except (IOError, os.error, DistutilsFileError) as why: except (IOError, os.error, DistutilsFileError) as why:
self.application.set_normal_cursor() self.application.set_normal_cursor()
@ -1375,9 +1388,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
log.info('No data copy requested') log.info('No data copy requested')
# Change the location of data directory in config file. # Change the location of data directory in config file.
settings = QtCore.QSettings() settings = QtCore.QSettings()
settings.setValue('advanced/data path', Path(self.new_data_path)) settings.setValue('advanced/data path', self.new_data_path)
# Check if the new data path is our default. # Check if the new data path is our default.
if self.new_data_path == str(AppLocation.get_directory(AppLocation.DataDir)): if self.new_data_path == AppLocation.get_directory(AppLocation.DataDir):
settings.remove('advanced/data path') settings.remove('advanced/data path')
self.application.set_normal_cursor() self.application.set_normal_cursor()

View File

@ -38,8 +38,7 @@ from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHE
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \ E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
from openlp.core.lib.projector.db import ProjectorDB from openlp.core.lib.projector.db import ProjectorDB
from openlp.core.lib.projector.pjlink import PJLink from openlp.core.lib.projector.pjlink import PJLink, PJLinkUDP
from openlp.core.lib.projector.pjlink2 import PJLinkUDP
from openlp.core.ui.projector.editform import ProjectorEditForm from openlp.core.ui.projector.editform import ProjectorEditForm
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
@ -700,16 +699,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjecto
:returns: PJLink() instance :returns: PJLink() instance
""" """
log.debug('_add_projector()') log.debug('_add_projector()')
return PJLink(dbid=projector.id, return PJLink(projector=projector,
ip=projector.ip,
port=int(projector.port),
name=projector.name,
location=projector.location,
notes=projector.notes,
pin=None if projector.pin == '' else projector.pin,
poll_time=self.poll_time, poll_time=self.poll_time,
socket_timeout=self.socket_timeout socket_timeout=self.socket_timeout)
)
def add_projector(self, projector, start=False): def add_projector(self, projector, start=False):
""" """

View File

@ -366,16 +366,20 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
""" """
return self._modified return self._modified
def set_file_name(self, file_name): def set_file_name(self, file_path):
""" """
Setter for service file. Setter for service file.
:param file_name: The service file name :param openlp.core.common.path.Path file_path: The service file name
:rtype: None
""" """
self._file_name = str(file_name) self._file_name = path_to_str(file_path)
self.main_window.set_service_modified(self.is_modified(), self.short_file_name()) self.main_window.set_service_modified(self.is_modified(), self.short_file_name())
Settings().setValue('servicemanager/last file', Path(file_name)) Settings().setValue('servicemanager/last file', file_path)
self._save_lite = self._file_name.endswith('.oszl') if file_path and file_path.suffix == '.oszl':
self._save_lite = True
else:
self._save_lite = False
def file_name(self): def file_name(self):
""" """
@ -474,7 +478,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
""" """
self.service_manager_list.clear() self.service_manager_list.clear()
self.service_items = [] self.service_items = []
self.set_file_name('') self.set_file_name(None)
self.service_id += 1 self.service_id += 1
self.set_modified(False) self.set_modified(False)
Settings().setValue('servicemanager/last file', None) Settings().setValue('servicemanager/last file', None)
@ -695,27 +699,25 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
default_file_name = format_time(default_pattern, local_time) default_file_name = format_time(default_pattern, local_time)
else: else:
default_file_name = '' default_file_name = ''
directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory')) default_file_path = Path(default_file_name)
path = os.path.join(directory, default_file_name) directory_path = Settings().value(self.main_window.service_manager_settings_section + '/last directory')
if directory_path:
default_file_path = directory_path / default_file_path
# SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in # SaveAs from osz to oszl is not valid as the files will be deleted on exit which is not sensible or usable in
# the long term. # the long term.
if self._file_name.endswith('oszl') or self.service_has_all_original_files: if self._file_name.endswith('oszl') or self.service_has_all_original_files:
file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( file_path, filter_used = FileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, path, self.main_window, UiStrings().SaveService, default_file_path,
translate('OpenLP.ServiceManager', translate('OpenLP.ServiceManager',
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)')) 'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
else: else:
file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( file_path, filter_used = FileDialog.getSaveFileName(
self.main_window, UiStrings().SaveService, path, self.main_window, UiStrings().SaveService, file_path,
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;')) translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
if not file_name: if not file_path:
return False return False
if os.path.splitext(file_name)[1] == '': file_path.with_suffix('.osz')
file_name += '.osz' self.set_file_name(file_path)
else:
ext = os.path.splitext(file_name)[1]
file_name.replace(ext, '.osz')
self.set_file_name(file_name)
self.decide_save_method() self.decide_save_method()
def decide_save_method(self, field=None): def decide_save_method(self, field=None):
@ -772,7 +774,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return return
file_to.close() file_to.close()
self.new_file() self.new_file()
self.set_file_name(file_name) self.set_file_name(str_to_path(file_name))
self.main_window.display_progress_bar(len(items)) self.main_window.display_progress_bar(len(items))
self.process_service_items(items) self.process_service_items(items)
delete_file(Path(p_file)) delete_file(Path(p_file))

View File

@ -28,7 +28,6 @@ import os
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui import ThemeLayoutForm from openlp.core.ui import ThemeLayoutForm
@ -61,7 +60,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setupUi(self) self.setupUi(self)
self.registerFields() self.registerFields()
self.update_theme_allowed = True self.update_theme_allowed = True
self.temp_background_filename = '' self.temp_background_filename = None
self.theme_layout_form = ThemeLayoutForm(self) self.theme_layout_form = ThemeLayoutForm(self)
self.background_combo_box.currentIndexChanged.connect(self.on_background_combo_box_current_index_changed) self.background_combo_box.currentIndexChanged.connect(self.on_background_combo_box_current_index_changed)
self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed) self.gradient_combo_box.currentIndexChanged.connect(self.on_gradient_combo_box_current_index_changed)
@ -188,8 +187,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
""" """
background_image = BackgroundType.to_string(BackgroundType.Image) background_image = BackgroundType.to_string(BackgroundType.Image)
if self.page(self.currentId()) == self.background_page and \ if self.page(self.currentId()) == self.background_page and \
self.theme.background_type == background_image and \ self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename):
is_not_image_file(Path(self.theme.background_filename)):
QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'), QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'),
translate('OpenLP.ThemeWizard', 'You have not selected a ' translate('OpenLP.ThemeWizard', 'You have not selected a '
'background image. Please select one before continuing.')) 'background image. Please select one before continuing.'))
@ -273,7 +271,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
Run the wizard. Run the wizard.
""" """
log.debug('Editing theme {name}'.format(name=self.theme.theme_name)) log.debug('Editing theme {name}'.format(name=self.theme.theme_name))
self.temp_background_filename = '' self.temp_background_filename = None
self.update_theme_allowed = False self.update_theme_allowed = False
self.set_defaults() self.set_defaults()
self.update_theme_allowed = True self.update_theme_allowed = True
@ -318,11 +316,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.setField('background_type', 1) self.setField('background_type', 1)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_color_button.color = self.theme.background_border_color self.image_color_button.color = self.theme.background_border_color
self.image_path_edit.path = str_to_path(self.theme.background_filename) self.image_path_edit.path = self.theme.background_filename
self.setField('background_type', 2) self.setField('background_type', 2)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
self.video_color_button.color = self.theme.background_border_color self.video_color_button.color = self.theme.background_border_color
self.video_path_edit.path = str_to_path(self.theme.background_filename) self.video_path_edit.path = self.theme.background_filename
self.setField('background_type', 4) self.setField('background_type', 4)
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent): elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
self.setField('background_type', 3) self.setField('background_type', 3)
@ -402,14 +400,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
self.theme.background_type = BackgroundType.to_string(index) self.theme.background_type = BackgroundType.to_string(index)
if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) and \ if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) and \
self.theme.background_type != BackgroundType.to_string(BackgroundType.Video) and \ self.theme.background_type != BackgroundType.to_string(BackgroundType.Video) and \
self.temp_background_filename == '': self.temp_background_filename is None:
self.temp_background_filename = self.theme.background_filename self.temp_background_filename = self.theme.background_filename
self.theme.background_filename = '' self.theme.background_filename = None
if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or
self.theme.background_type != BackgroundType.to_string(BackgroundType.Video)) and \ self.theme.background_type != BackgroundType.to_string(BackgroundType.Video)) and \
self.temp_background_filename != '': self.temp_background_filename is not None:
self.theme.background_filename = self.temp_background_filename self.theme.background_filename = self.temp_background_filename
self.temp_background_filename = '' self.temp_background_filename = None
self.set_background_page_values() self.set_background_page_values()
def on_gradient_combo_box_current_index_changed(self, index): def on_gradient_combo_box_current_index_changed(self, index):
@ -450,18 +448,24 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
""" """
self.theme.background_end_color = color self.theme.background_end_color = color
def on_image_path_edit_path_changed(self, file_path): def on_image_path_edit_path_changed(self, new_path):
""" """
Background Image button pushed. Handle the `pathEditChanged` signal from image_path_edit
:param openlp.core.common.path.Path new_path: Path to the new image
:rtype: None
""" """
self.theme.background_filename = path_to_str(file_path) self.theme.background_filename = new_path
self.set_background_page_values() self.set_background_page_values()
def on_video_path_edit_path_changed(self, file_path): def on_video_path_edit_path_changed(self, new_path):
""" """
Background video button pushed. Handle the `pathEditChanged` signal from video_path_edit
:param openlp.core.common.path.Path new_path: Path to the new video
:rtype: None
""" """
self.theme.background_filename = path_to_str(file_path) self.theme.background_filename = new_path
self.set_background_page_values() self.set_background_page_values()
def on_main_color_changed(self, color): def on_main_color_changed(self, color):
@ -537,14 +541,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
translate('OpenLP.ThemeWizard', 'Theme Name Invalid'), translate('OpenLP.ThemeWizard', 'Theme Name Invalid'),
translate('OpenLP.ThemeWizard', 'Invalid theme name. Please enter one.')) translate('OpenLP.ThemeWizard', 'Invalid theme name. Please enter one.'))
return return
save_from = None source_path = None
save_to = None destination_path = None
if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \ if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \
self.theme.background_type == BackgroundType.to_string(BackgroundType.Video): self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
filename = os.path.split(str(self.theme.background_filename))[1] file_name = self.theme.background_filename.name
save_to = os.path.join(self.path, self.theme.theme_name, filename) destination_path = self.path / self.theme.theme_name / file_name
save_from = self.theme.background_filename source_path = self.theme.background_filename
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name): if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
return return
self.theme_manager.save_theme(self.theme, save_from, save_to) self.theme_manager.save_theme(self.theme, source_path, destination_path)
return QtWidgets.QDialog.accept(self) return QtWidgets.QDialog.accept(self)

View File

@ -24,14 +24,14 @@ The Theme Manager manages adding, deleteing and modifying of themes.
""" """
import os import os
import zipfile import zipfile
import shutil
from xml.etree.ElementTree import ElementTree, XML from xml.etree.ElementTree import ElementTree, XML
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file UiStrings, check_directory_exists, translate, delete_file
from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.languagemanager import get_locale_key
from openlp.core.common.path import Path, copyfile, path_to_str, rmtree
from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \ from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
check_item_selected, create_thumb, validate_thumb check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.theme import Theme, BackgroundType from openlp.core.lib.theme import Theme, BackgroundType
@ -39,7 +39,6 @@ from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.ui import FileRenameForm, ThemeForm
from openlp.core.ui.lib import OpenLPToolbar from openlp.core.ui.lib import OpenLPToolbar
from openlp.core.ui.lib.filedialog import FileDialog from openlp.core.ui.lib.filedialog import FileDialog
from openlp.core.common.languagemanager import get_locale_key
class Ui_ThemeManager(object): class Ui_ThemeManager(object):
@ -135,7 +134,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
self.settings_section = 'themes' self.settings_section = 'themes'
# Variables # Variables
self.theme_list = [] self.theme_list = []
self.old_background_image = None self.old_background_image_path = None
def bootstrap_initialise(self): def bootstrap_initialise(self):
""" """
@ -145,25 +144,41 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
self.global_theme = Settings().value(self.settings_section + '/global theme') self.global_theme = Settings().value(self.settings_section + '/global theme')
self.build_theme_path() self.build_theme_path()
self.load_first_time_themes() self.load_first_time_themes()
self.upgrade_themes()
def bootstrap_post_set_up(self): def bootstrap_post_set_up(self):
""" """
process the bootstrap post setup request process the bootstrap post setup request
""" """
self.theme_form = ThemeForm(self) self.theme_form = ThemeForm(self)
self.theme_form.path = self.path self.theme_form.path = self.theme_path
self.file_rename_form = FileRenameForm() self.file_rename_form = FileRenameForm()
Registry().register_function('theme_update_global', self.change_global_from_tab) Registry().register_function('theme_update_global', self.change_global_from_tab)
self.load_themes() self.load_themes()
def upgrade_themes(self):
"""
Upgrade the xml files to json.
:rtype: None
"""
xml_file_paths = AppLocation.get_section_data_path('themes').glob('*/*.xml')
for xml_file_path in xml_file_paths:
theme_data = get_text_file_string(xml_file_path)
theme = self._create_theme_from_xml(theme_data, self.theme_path)
self._write_theme(theme)
xml_file_path.unlink()
def build_theme_path(self): def build_theme_path(self):
""" """
Set up the theme path variables Set up the theme path variables
:rtype: None
""" """
self.path = str(AppLocation.get_section_data_path(self.settings_section)) self.theme_path = AppLocation.get_section_data_path(self.settings_section)
check_directory_exists(Path(self.path)) check_directory_exists(self.theme_path)
self.thumb_path = os.path.join(self.path, 'thumbnails') self.thumb_path = self.theme_path / 'thumbnails'
check_directory_exists(Path(self.thumb_path)) check_directory_exists(self.thumb_path)
def check_list_state(self, item, field=None): def check_list_state(self, item, field=None):
""" """
@ -298,17 +313,18 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
Takes a theme and makes a new copy of it as well as saving it. Takes a theme and makes a new copy of it as well as saving it.
:param theme_data: The theme to be used :param Theme theme_data: The theme to be used
:param new_theme_name: The new theme name to save the data to :param str new_theme_name: The new theme name of the theme
:rtype: None
""" """
save_to = None destination_path = None
save_from = None source_path = None
if theme_data.background_type == 'image' or theme_data.background_type == 'video': if theme_data.background_type == 'image' or theme_data.background_type == 'video':
save_to = os.path.join(self.path, new_theme_name, os.path.split(str(theme_data.background_filename))[1]) destination_path = self.theme_path / new_theme_name / theme_data.background_filename.name
save_from = theme_data.background_filename source_path = theme_data.background_filename
theme_data.theme_name = new_theme_name theme_data.theme_name = new_theme_name
theme_data.extend_image_filename(self.path) theme_data.extend_image_filename(self.theme_path)
self.save_theme(theme_data, save_from, save_to) self.save_theme(theme_data, source_path, destination_path)
self.load_themes() self.load_themes()
def on_edit_theme(self, field=None): def on_edit_theme(self, field=None):
@ -322,10 +338,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
item = self.theme_list_widget.currentItem() item = self.theme_list_widget.currentItem()
theme = self.get_theme_data(item.data(QtCore.Qt.UserRole)) theme = self.get_theme_data(item.data(QtCore.Qt.UserRole))
if theme.background_type == 'image' or theme.background_type == 'video': if theme.background_type == 'image' or theme.background_type == 'video':
self.old_background_image = theme.background_filename self.old_background_image_path = theme.background_filename
self.theme_form.theme = theme self.theme_form.theme = theme
self.theme_form.exec(True) self.theme_form.exec(True)
self.old_background_image = None self.old_background_image_path = None
self.renderer.update_theme(theme.theme_name) self.renderer.update_theme(theme.theme_name)
self.load_themes() self.load_themes()
@ -355,77 +371,76 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
self.theme_list.remove(theme) self.theme_list.remove(theme)
thumb = '{name}.png'.format(name=theme) thumb = '{name}.png'.format(name=theme)
delete_file(Path(self.path, thumb)) delete_file(self.theme_path / thumb)
delete_file(Path(self.thumb_path, thumb)) delete_file(self.thumb_path / thumb)
try: try:
# Windows is always unicode, so no need to encode filenames rmtree(self.theme_path / theme)
if is_win(): except OSError:
shutil.rmtree(os.path.join(self.path, theme))
else:
encoding = get_filesystem_encoding()
shutil.rmtree(os.path.join(self.path, theme).encode(encoding))
except OSError as os_error:
shutil.Error = os_error
self.log_exception('Error deleting theme {name}'.format(name=theme)) self.log_exception('Error deleting theme {name}'.format(name=theme))
def on_export_theme(self, field=None): def on_export_theme(self, checked=None):
""" """
Export the theme in a zip file Export the theme to a zip file
:param field:
:param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
:rtype: None
""" """
item = self.theme_list_widget.currentItem() item = self.theme_list_widget.currentItem()
if item is None: if item is None:
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.')) critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
return return
theme = item.data(QtCore.Qt.UserRole) theme_name = item.data(QtCore.Qt.UserRole)
export_path, filter_used = \ export_path, filter_used = \
FileDialog.getSaveFileName(self.main_window, FileDialog.getSaveFileName(self.main_window,
translate('OpenLP.ThemeManager', 'Save Theme - ({name})'). translate('OpenLP.ThemeManager',
format(name=theme), 'Save Theme - ({name})').format(name=theme_name),
Settings().value(self.settings_section + '/last directory export'), Settings().value(self.settings_section + '/last directory export'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)')) translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
self.application.set_busy_cursor() self.application.set_busy_cursor()
if export_path: if export_path:
Settings().setValue(self.settings_section + '/last directory export', export_path.parent) Settings().setValue(self.settings_section + '/last directory export', export_path.parent)
if self._export_theme(str(export_path), theme): if self._export_theme(export_path.with_suffix('.otz'), theme_name):
QtWidgets.QMessageBox.information(self, QtWidgets.QMessageBox.information(self,
translate('OpenLP.ThemeManager', 'Theme Exported'), translate('OpenLP.ThemeManager', 'Theme Exported'),
translate('OpenLP.ThemeManager', translate('OpenLP.ThemeManager',
'Your theme has been successfully exported.')) 'Your theme has been successfully exported.'))
self.application.set_normal_cursor() self.application.set_normal_cursor()
def _export_theme(self, theme_path, theme): def _export_theme(self, theme_path, theme_name):
""" """
Create the zipfile with the theme contents. Create the zipfile with the theme contents.
:param theme_path: Location where the zip file will be placed
:param theme: The name of the theme to be exported :param openlp.core.common.path.Path theme_path: Location where the zip file will be placed
:param str theme_name: The name of the theme to be exported
:return: The success of creating the zip file
:rtype: bool
""" """
theme_zip = None
try: try:
theme_zip = zipfile.ZipFile(theme_path, 'w') with zipfile.ZipFile(str(theme_path), 'w') as theme_zip:
source = os.path.join(self.path, theme) source_path = self.theme_path / theme_name
for files in os.walk(source): for file_path in source_path.iterdir():
for name in files[2]: theme_zip.write(str(file_path), os.path.join(theme_name, file_path.name))
theme_zip.write(os.path.join(source, name), os.path.join(theme, name))
theme_zip.close()
return True return True
except OSError as ose: except OSError as ose:
self.log_exception('Export Theme Failed') self.log_exception('Export Theme Failed')
critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'), critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'),
translate('OpenLP.ThemeManager', 'The theme export failed because this error ' translate('OpenLP.ThemeManager',
'occurred: {err}').format(err=ose.strerror)) 'The theme_name export failed because this error occurred: {err}')
if theme_zip: .format(err=ose.strerror))
theme_zip.close() if theme_path.exists():
shutil.rmtree(theme_path, True) rmtree(theme_path, True)
return False return False
def on_import_theme(self, field=None): def on_import_theme(self, checked=None):
""" """
Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from Opens a file dialog to select the theme file(s) to import before attempting to extract OpenLP themes from
those files. This process will only load version 2 themes. those files. This process will only load version 2 themes.
:param field:
:param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
:rtype: None
""" """
file_paths, selected_filter = FileDialog.getOpenFileNames( file_paths, filter_used = FileDialog.getOpenFileNames(
self, self,
translate('OpenLP.ThemeManager', 'Select Theme Import File'), translate('OpenLP.ThemeManager', 'Select Theme Import File'),
Settings().value(self.settings_section + '/last directory import'), Settings().value(self.settings_section + '/last directory import'),
@ -435,8 +450,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
return return
self.application.set_busy_cursor() self.application.set_busy_cursor()
for file_path in file_paths: for file_path in file_paths:
self.unzip_theme(path_to_str(file_path), self.path) self.unzip_theme(file_path, self.theme_path)
Settings().setValue(self.settings_section + '/last directory import', file_path) Settings().setValue(self.settings_section + '/last directory import', file_path.parent)
self.load_themes() self.load_themes()
self.application.set_normal_cursor() self.application.set_normal_cursor()
@ -445,17 +460,17 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
Imports any themes on start up and makes sure there is at least one theme Imports any themes on start up and makes sure there is at least one theme
""" """
self.application.set_busy_cursor() self.application.set_busy_cursor()
files = AppLocation.get_files(self.settings_section, '.otz') theme_paths = AppLocation.get_files(self.settings_section, '.otz')
for theme_file in files: for theme_path in theme_paths:
theme_file = os.path.join(self.path, str(theme_file)) theme_path = self.theme_path / theme_path
self.unzip_theme(theme_file, self.path) self.unzip_theme(theme_path, self.theme_path)
delete_file(Path(theme_file)) delete_file(theme_path)
files = AppLocation.get_files(self.settings_section, '.png') theme_paths = AppLocation.get_files(self.settings_section, '.png')
# No themes have been found so create one # No themes have been found so create one
if not files: if not theme_paths:
theme = Theme() theme = Theme()
theme.theme_name = UiStrings().Default theme.theme_name = UiStrings().Default
self._write_theme(theme, None, None) self._write_theme(theme)
Settings().setValue(self.settings_section + '/global theme', theme.theme_name) Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
self.application.set_normal_cursor() self.application.set_normal_cursor()
@ -471,22 +486,21 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
# Sort the themes by its name considering language specific # Sort the themes by its name considering language specific
files.sort(key=lambda file_name: get_locale_key(str(file_name))) files.sort(key=lambda file_name: get_locale_key(str(file_name)))
# now process the file list of png files # now process the file list of png files
for name in files: for file in files:
name = str(name)
# check to see file is in theme root directory # check to see file is in theme root directory
theme = os.path.join(self.path, name) theme_path = self.theme_path / file
if os.path.exists(theme): if theme_path.exists():
text_name = os.path.splitext(name)[0] text_name = theme_path.stem
if text_name == self.global_theme: if text_name == self.global_theme:
name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name) name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
else: else:
name = text_name name = text_name
thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=text_name)) thumb = self.thumb_path / '{name}.png'.format(name=text_name)
item_name = QtWidgets.QListWidgetItem(name) item_name = QtWidgets.QListWidgetItem(name)
if validate_thumb(theme, thumb): if validate_thumb(theme_path, thumb):
icon = build_icon(thumb) icon = build_icon(thumb)
else: else:
icon = create_thumb(theme, thumb) icon = create_thumb(str(theme_path), str(thumb))
item_name.setIcon(icon) item_name.setIcon(icon)
item_name.setData(QtCore.Qt.UserRole, text_name) item_name.setData(QtCore.Qt.UserRole, text_name)
self.theme_list_widget.addItem(item_name) self.theme_list_widget.addItem(item_name)
@ -507,27 +521,19 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
def get_theme_data(self, theme_name): def get_theme_data(self, theme_name):
""" """
Returns a theme object from an XML or JSON file Returns a theme object from a JSON file
:param theme_name: Name of the theme to load from file :param str theme_name: Name of the theme to load from file
:return: The theme object. :return: The theme object.
:rtype: Theme
""" """
self.log_debug('get theme data for theme {name}'.format(name=theme_name)) theme_name = str(theme_name)
theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name)) theme_file_path = self.theme_path / theme_name / '{file_name}.json'.format(file_name=theme_name)
theme_data = get_text_file_string(theme_file_path) theme_data = get_text_file_string(theme_file_path)
jsn = True
if not theme_data:
theme_file_path = theme_file_path.with_suffix('.xml')
theme_data = get_text_file_string(theme_file_path)
jsn = False
if not theme_data: if not theme_data:
self.log_debug('No theme data - using default theme') self.log_debug('No theme data - using default theme')
return Theme() return Theme()
else: return self._create_theme_from_json(theme_data, self.theme_path)
if jsn:
return self._create_theme_from_json(theme_data, self.path)
else:
return self._create_theme_from_xml(theme_data, self.path)
def over_write_message_box(self, theme_name): def over_write_message_box(self, theme_name):
""" """
@ -543,172 +549,148 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
defaultButton=QtWidgets.QMessageBox.No) defaultButton=QtWidgets.QMessageBox.No)
return ret == QtWidgets.QMessageBox.Yes return ret == QtWidgets.QMessageBox.Yes
def unzip_theme(self, file_name, directory): def unzip_theme(self, file_path, directory_path):
""" """
Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version Unzip the theme, remove the preview file if stored. Generate a new preview file. Check the XML theme version
and upgrade if necessary. and upgrade if necessary.
:param file_name: :param openlp.core.common.path.Path file_path:
:param directory: :param openlp.core.common.path.Path directory_path:
""" """
self.log_debug('Unzipping theme {name}'.format(name=file_name)) self.log_debug('Unzipping theme {name}'.format(name=file_path))
theme_zip = None
out_file = None
file_xml = None file_xml = None
abort_import = True abort_import = True
json_theme = False json_theme = False
theme_name = "" theme_name = ""
try: try:
theme_zip = zipfile.ZipFile(file_name) with zipfile.ZipFile(str(file_path)) as theme_zip:
json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json'] json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
if len(json_file) != 1: if len(json_file) != 1:
# TODO: remove XML handling at some point but would need a auto conversion to run first. # TODO: remove XML handling after the 2.6 release.
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml'] xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
if len(xml_file) != 1: if len(xml_file) != 1:
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file))) self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
raise ValidationError raise ValidationError
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot() xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
theme_version = xml_tree.get('version', default=None) theme_version = xml_tree.get('version', default=None)
if not theme_version or float(theme_version) < 2.0: if not theme_version or float(theme_version) < 2.0:
self.log_error('Theme version is less than 2.0') self.log_error('Theme version is less than 2.0')
raise ValidationError raise ValidationError
theme_name = xml_tree.find('name').text.strip() theme_name = xml_tree.find('name').text.strip()
else:
new_theme = Theme()
new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
theme_name = new_theme.theme_name
json_theme = True
theme_folder = os.path.join(directory, theme_name)
theme_exists = os.path.exists(theme_folder)
if theme_exists and not self.over_write_message_box(theme_name):
abort_import = True
return
else:
abort_import = False
for name in theme_zip.namelist():
out_name = name.replace('/', os.path.sep)
split_name = out_name.split(os.path.sep)
if split_name[-1] == '' or len(split_name) == 1:
# is directory or preview file
continue
full_name = os.path.join(directory, out_name)
check_directory_exists(Path(os.path.dirname(full_name)))
if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
file_xml = str(theme_zip.read(name), 'utf-8')
out_file = open(full_name, 'w', encoding='utf-8')
out_file.write(file_xml)
else: else:
out_file = open(full_name, 'wb') new_theme = Theme()
out_file.write(theme_zip.read(name)) new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
out_file.close() theme_name = new_theme.theme_name
json_theme = True
theme_folder = directory_path / theme_name
if theme_folder.exists() and not self.over_write_message_box(theme_name):
abort_import = True
return
else:
abort_import = False
for zipped_file in theme_zip.namelist():
zipped_file_rel_path = Path(zipped_file)
split_name = zipped_file_rel_path.parts
if split_name[-1] == '' or len(split_name) == 1:
# is directory or preview file
continue
full_name = directory_path / zipped_file_rel_path
check_directory_exists(full_name.parent)
if zipped_file_rel_path.suffix.lower() == '.xml' or zipped_file_rel_path.suffix.lower() == '.json':
file_xml = str(theme_zip.read(zipped_file), 'utf-8')
with full_name.open('w', encoding='utf-8') as out_file:
out_file.write(file_xml)
else:
with full_name.open('wb') as out_file:
out_file.write(theme_zip.read(zipped_file))
except (IOError, zipfile.BadZipfile): except (IOError, zipfile.BadZipfile):
self.log_exception('Importing theme from zip failed {name}'.format(name=file_name)) self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
raise ValidationError raise ValidationError
except ValidationError: except ValidationError:
critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'), critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
translate('OpenLP.ThemeManager', 'File is not a valid theme.')) translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
finally: finally:
# Close the files, to be able to continue creating the theme.
if theme_zip:
theme_zip.close()
if out_file:
out_file.close()
if not abort_import: if not abort_import:
# As all files are closed, we can create the Theme. # As all files are closed, we can create the Theme.
if file_xml: if file_xml:
if json_theme: if json_theme:
theme = self._create_theme_from_json(file_xml, self.path) theme = self._create_theme_from_json(file_xml, self.theme_path)
else: else:
theme = self._create_theme_from_xml(file_xml, self.path) theme = self._create_theme_from_xml(file_xml, self.theme_path)
self.generate_and_save_image(theme_name, theme) self.generate_and_save_image(theme_name, theme)
# Only show the error message, when IOError was not raised (in
# this case the error message has already been shown).
elif theme_zip is not None:
critical_error_message_box(
translate('OpenLP.ThemeManager', 'Validation Error'),
translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
self.log_error('Theme file does not contain XML data {name}'.format(name=file_name))
def check_if_theme_exists(self, theme_name): def check_if_theme_exists(self, theme_name):
""" """
Check if theme already exists and displays error message Check if theme already exists and displays error message
:param theme_name: Name of the Theme to test :param str theme_name: Name of the Theme to test
:return: True or False if theme exists :return: True or False if theme exists
:rtype: bool
""" """
theme_dir = os.path.join(self.path, theme_name) if (self.theme_path / theme_name).exists():
if os.path.exists(theme_dir):
critical_error_message_box( critical_error_message_box(
translate('OpenLP.ThemeManager', 'Validation Error'), translate('OpenLP.ThemeManager', 'Validation Error'),
translate('OpenLP.ThemeManager', 'A theme with this name already exists.')) translate('OpenLP.ThemeManager', 'A theme with this name already exists.'))
return False return False
return True return True
def save_theme(self, theme, image_from, image_to): def save_theme(self, theme, image_source_path, image_destination_path):
""" """
Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list Called by theme maintenance Dialog to save the theme and to trigger the reload of the theme list
:param theme: The theme data object. :param Theme theme: The theme data object.
:param image_from: Where the theme image is currently located. :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
:param image_to: Where the Theme Image is to be saved to :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
:rtype: None
""" """
self._write_theme(theme, image_from, image_to) self._write_theme(theme, image_source_path, image_destination_path)
if theme.background_type == BackgroundType.to_string(BackgroundType.Image): if theme.background_type == BackgroundType.to_string(BackgroundType.Image):
self.image_manager.update_image_border(theme.background_filename, self.image_manager.update_image_border(path_to_str(theme.background_filename),
ImageSource.Theme, ImageSource.Theme,
QtGui.QColor(theme.background_border_color)) QtGui.QColor(theme.background_border_color))
self.image_manager.process_updates() self.image_manager.process_updates()
def _write_theme(self, theme, image_from, image_to): def _write_theme(self, theme, image_source_path=None, image_destination_path=None):
""" """
Writes the theme to the disk and handles the background image if necessary Writes the theme to the disk and handles the background image if necessary
:param theme: The theme data object. :param Theme theme: The theme data object.
:param image_from: Where the theme image is currently located. :param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
:param image_to: Where the Theme Image is to be saved to :param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
:rtype: None
""" """
name = theme.theme_name name = theme.theme_name
theme_pretty = theme.export_theme() theme_pretty = theme.export_theme(self.theme_path)
theme_dir = os.path.join(self.path, name) theme_dir = self.theme_path / name
check_directory_exists(Path(theme_dir)) check_directory_exists(theme_dir)
theme_file = os.path.join(theme_dir, name + '.json') theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
if self.old_background_image and image_to != self.old_background_image:
delete_file(Path(self.old_background_image))
out_file = None
try: try:
out_file = open(theme_file, 'w', encoding='utf-8') theme_path.write_text(theme_pretty)
out_file.write(theme_pretty)
except IOError: except IOError:
self.log_exception('Saving theme to file failed') self.log_exception('Saving theme to file failed')
finally: if image_source_path and image_destination_path:
if out_file: if self.old_background_image_path and image_destination_path != self.old_background_image_path:
out_file.close() delete_file(self.old_background_image_path)
if image_from and os.path.abspath(image_from) != os.path.abspath(image_to): if image_source_path != image_destination_path:
try: try:
# Windows is always unicode, so no need to encode filenames copyfile(image_source_path, image_destination_path)
if is_win(): except IOError:
shutil.copyfile(image_from, image_to) self.log_exception('Failed to save theme image')
else:
encoding = get_filesystem_encoding()
shutil.copyfile(image_from.encode(encoding), image_to.encode(encoding))
except IOError as xxx_todo_changeme:
shutil.Error = xxx_todo_changeme
self.log_exception('Failed to save theme image')
self.generate_and_save_image(name, theme) self.generate_and_save_image(name, theme)
def generate_and_save_image(self, name, theme): def generate_and_save_image(self, theme_name, theme):
""" """
Generate and save a preview image Generate and save a preview image
:param name: The name of the theme. :param str theme_name: The name of the theme.
:param theme: The theme data object. :param theme: The theme data object.
""" """
frame = self.generate_image(theme) frame = self.generate_image(theme)
sample_path_name = os.path.join(self.path, name + '.png') sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name)
if os.path.exists(sample_path_name): if sample_path_name.exists():
os.unlink(sample_path_name) sample_path_name.unlink()
frame.save(sample_path_name, 'png') frame.save(str(sample_path_name), 'png')
thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=name)) thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
create_thumb(sample_path_name, thumb, False) create_thumb(str(sample_path_name), str(thumb_path), False)
def update_preview_images(self): def update_preview_images(self):
""" """
@ -730,39 +712,32 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
""" """
return self.renderer.generate_preview(theme_data, force_page) return self.renderer.generate_preview(theme_data, force_page)
def get_preview_image(self, theme):
"""
Return an image representing the look of the theme
:param theme: The theme to return the image for.
"""
return os.path.join(self.path, theme + '.png')
@staticmethod @staticmethod
def _create_theme_from_xml(theme_xml, image_path): def _create_theme_from_xml(theme_xml, image_path):
""" """
Return a theme object using information parsed from XML Return a theme object using information parsed from XML
:param theme_xml: The Theme data object. :param theme_xml: The Theme data object.
:param image_path: Where the theme image is stored :param openlp.core.common.path.Path image_path: Where the theme image is stored
:return: Theme data. :return: Theme data.
:rtype: Theme
""" """
theme = Theme() theme = Theme()
theme.parse(theme_xml) theme.parse(theme_xml)
theme.extend_image_filename(image_path) theme.extend_image_filename(image_path)
return theme return theme
@staticmethod def _create_theme_from_json(self, theme_json, image_path):
def _create_theme_from_json(theme_json, image_path):
""" """
Return a theme object using information parsed from JSON Return a theme object using information parsed from JSON
:param theme_json: The Theme data object. :param theme_json: The Theme data object.
:param image_path: Where the theme image is stored :param openlp.core.common.path.Path image_path: Where the theme image is stored
:return: Theme data. :return: Theme data.
:rtype: Theme
""" """
theme = Theme() theme = Theme()
theme.load_theme(theme_json) theme.load_theme(theme_json, self.theme_path)
theme.extend_image_filename(image_path) theme.extend_image_filename(image_path)
return theme return theme

View File

@ -211,8 +211,8 @@ class ThemesTab(SettingsTab):
""" """
Utility method to update the global theme preview image. Utility method to update the global theme preview image.
""" """
image = self.theme_manager.get_preview_image(self.global_theme) image_path = self.theme_manager.theme_path / '{file_name}.png'.format(file_name=self.global_theme)
preview = QtGui.QPixmap(str(image)) preview = QtGui.QPixmap(str(image_path))
if not preview.isNull(): if not preview.isNull():
preview = preview.scaled(300, 255, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) preview = preview.scaled(300, 255, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.default_list_view.setPixmap(preview) self.default_list_view.setPixmap(preview)

View File

@ -20,24 +20,22 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
""" """
The :mod:`openlp.core.common` module downloads the version details for OpenLP. The :mod:`openlp.core.version` module downloads the version details for OpenLP.
""" """
import logging import logging
import os import os
import platform import platform
import sys import sys
import time import time
import urllib.error from datetime import date
import urllib.parse
import urllib.request
from datetime import datetime
from distutils.version import LooseVersion from distutils.version import LooseVersion
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import requests
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import AppLocation, Registry, Settings from openlp.core.common import AppLocation, Settings
from openlp.core.common.httputils import ping from openlp.core.threading import run_thread
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -46,42 +44,93 @@ CONNECTION_TIMEOUT = 30
CONNECTION_RETRIES = 2 CONNECTION_RETRIES = 2
class VersionThread(QtCore.QThread): class VersionWorker(QtCore.QObject):
""" """
A special Qt thread class to fetch the version of OpenLP from the website. A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it
This is threaded so that it doesn't affect the loading time of OpenLP. doesn't affect the loading time of OpenLP.
""" """
def __init__(self, main_window): new_version = QtCore.pyqtSignal(dict)
""" no_internet = QtCore.pyqtSignal()
Constructor for the thread class. quit = QtCore.pyqtSignal()
:param main_window: The main window Object. def __init__(self, last_check_date, current_version):
""" """
log.debug("VersionThread - Initialise") Constructor for the version check worker.
super(VersionThread, self).__init__(None)
self.main_window = main_window
def run(self): :param string last_check_date: The last day we checked for a new version of OpenLP
""" """
Run the thread. log.debug('VersionWorker - Initialise')
super(VersionWorker, self).__init__(None)
self.last_check_date = last_check_date
self.current_version = current_version
def start(self):
""" """
self.sleep(1) Check the latest version of OpenLP against the version file on the OpenLP site.
log.debug('Version thread - run')
found = ping("openlp.io") **Rules around versions and version files:**
Registry().set_flag('internet_present', found)
update_check = Settings().value('core/update check') * If a version number has a build (i.e. -bzr1234), then it is a nightly.
if found: * If a version number's minor version is an odd number, it is a development release.
Registry().execute('get_website_version') * If a version number's minor version is an even number, it is a stable release.
if update_check: """
app_version = get_application_version() log.debug('VersionWorker - Start')
version = check_latest_version(app_version) # I'm not entirely sure why this was here, I'm commenting it out until I hit the same scenario
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)), time.sleep(1)
version2=LooseVersion(str(app_version['full'])))) download_url = 'http://www.openlp.org/files/version.txt'
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])): if self.current_version['build']:
self.main_window.openlp_version_check.emit('{version}'.format(version=version)) download_url = 'http://www.openlp.org/files/nightly_version.txt'
elif int(self.current_version['version'].split('.')[1]) % 2 != 0:
download_url = 'http://www.openlp.org/files/dev_version.txt'
headers = {
'User-Agent': 'OpenLP/{version} {system}/{release}; '.format(version=self.current_version['full'],
system=platform.system(),
release=platform.release())
}
remote_version = None
retries = 0
while retries < 3:
try:
response = requests.get(download_url, headers=headers)
remote_version = response.text
log.debug('New version found: %s', remote_version)
break
except IOError:
log.exception('Unable to connect to OpenLP server to download version file')
retries += 1
else:
self.no_internet.emit()
if remote_version and LooseVersion(remote_version) > LooseVersion(self.current_version['full']):
self.new_version.emit(remote_version)
self.quit.emit()
def get_application_version(): def update_check_date():
"""
Save when we last checked for an update
"""
Settings().setValue('core/last version test', date.today().strftime('%Y-%m-%d'))
def check_for_update(parent):
"""
Run a thread to download and check the version of OpenLP
:param MainWindow parent: The parent object for the thread. Usually the OpenLP main window.
"""
last_check_date = Settings().value('core/last version test')
if date.today().strftime('%Y-%m-%d') <= last_check_date:
log.debug('Version check skipped, last checked today')
return
worker = VersionWorker(last_check_date, get_version())
worker.new_version.connect(parent.on_new_version)
worker.quit.connect(update_check_date)
# TODO: Use this to figure out if there's an Internet connection?
# worker.no_internet.connect(parent.on_no_internet)
run_thread(parent, worker, 'version')
def get_version():
""" """
Returns the application version of the running instance of OpenLP:: Returns the application version of the running instance of OpenLP::
@ -150,55 +199,3 @@ def get_application_version():
else: else:
log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version'])) log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version']))
return APPLICATION_VERSION return APPLICATION_VERSION
def check_latest_version(current_version):
"""
Check the latest version of OpenLP against the version file on the OpenLP
site.
**Rules around versions and version files:**
* If a version number has a build (i.e. -bzr1234), then it is a nightly.
* If a version number's minor version is an odd number, it is a development release.
* If a version number's minor version is an even number, it is a stable release.
:param current_version: The current version of OpenLP.
"""
version_string = current_version['full']
# set to prod in the distribution config file.
settings = Settings()
settings.beginGroup('core')
last_test = settings.value('last version test')
this_test = str(datetime.now().date())
settings.setValue('last version test', this_test)
settings.endGroup()
if last_test != this_test:
if current_version['build']:
req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
else:
version_parts = current_version['version'].split('.')
if int(version_parts[1]) % 2 != 0:
req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
else:
req = urllib.request.Request('http://www.openlp.org/files/version.txt')
req.add_header('User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'],
system=platform.system(),
release=platform.release()))
remote_version = None
retries = 0
while True:
try:
remote_version = str(urllib.request.urlopen(req, None,
timeout=CONNECTION_TIMEOUT).read().decode()).strip()
except (urllib.error.URLError, ConnectionError):
if retries > CONNECTION_RETRIES:
log.exception('Failed to download the latest OpenLP version file')
else:
retries += 1
time.sleep(0.1)
continue
break
if remote_version:
version_string = remote_version
return version_string

View File

@ -70,7 +70,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
item_name = QtWidgets.QListWidgetItem(alert.text) item_name = QtWidgets.QListWidgetItem(alert.text)
item_name.setData(QtCore.Qt.UserRole, alert.id) item_name.setData(QtCore.Qt.UserRole, alert.id)
self.alert_list_widget.addItem(item_name) self.alert_list_widget.addItem(item_name)
if alert.text == str(self.alert_text_edit.text()): if alert.text == self.alert_text_edit.text():
self.item_id = alert.id self.item_id = alert.id
self.alert_list_widget.setCurrentRow(self.alert_list_widget.row(item_name)) self.alert_list_widget.setCurrentRow(self.alert_list_widget.row(item_name))

View File

@ -32,9 +32,6 @@ class AlertsTab(SettingsTab):
""" """
AlertsTab is the alerts settings tab in the settings dialog. AlertsTab is the alerts settings tab in the settings dialog.
""" """
def __init__(self, parent, name, visible_title, icon_path):
super(AlertsTab, self).__init__(parent, name, visible_title, icon_path)
def setupUi(self): def setupUi(self):
self.setObjectName('AlertsTab') self.setObjectName('AlertsTab')
super(AlertsTab, self).setupUi() super(AlertsTab, self).setupUi()

View File

@ -93,7 +93,7 @@ class BGExtract(RegistryProperties):
NAME = 'BibleGateway' NAME = 'BibleGateway'
def __init__(self, proxy_url=None): def __init__(self, proxy_url=None):
log.debug('BGExtract.init("{url}")'.format(url=proxy_url)) log.debug('BGExtract.init(proxy_url="{url}")'.format(url=proxy_url))
self.proxy_url = proxy_url self.proxy_url = proxy_url
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
@ -285,15 +285,10 @@ class BGExtract(RegistryProperties):
log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version)) log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version))
url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)}) url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)})
reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params) reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params)
page = get_web_page(reference_url) page_source = get_web_page(reference_url)
if not page: if not page_source:
send_error_message('download') send_error_message('download')
return None return None
page_source = page.read()
try:
page_source = str(page_source, 'utf8')
except UnicodeDecodeError:
page_source = str(page_source, 'cp1251')
try: try:
soup = BeautifulSoup(page_source, 'lxml') soup = BeautifulSoup(page_source, 'lxml')
except Exception: except Exception:
@ -759,7 +754,7 @@ class HTTPBible(BibleImport, RegistryProperties):
return BiblesResourcesDB.get_verse_count(book_id, chapter) return BiblesResourcesDB.get_verse_count(book_id, chapter)
def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None): def get_soup_for_bible_ref(reference_url, headers=None, pre_parse_regex=None, pre_parse_substitute=None):
""" """
Gets a webpage and returns a parsed and optionally cleaned soup or None. Gets a webpage and returns a parsed and optionally cleaned soup or None.
@ -772,15 +767,15 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
if not reference_url: if not reference_url:
return None return None
try: try:
page = get_web_page(reference_url, header, True) page_source = get_web_page(reference_url, headers, update_openlp=True)
except Exception as e: except Exception as e:
page = None log.exception('Unable to download Bible %s, unknown exception occurred', reference_url)
if not page: page_source = None
if not page_source:
send_error_message('download') send_error_message('download')
return None return None
page_source = page.read()
if pre_parse_regex and pre_parse_substitute is not None: if pre_parse_regex and pre_parse_substitute is not None:
page_source = re.sub(pre_parse_regex, pre_parse_substitute, page_source.decode()) page_source = re.sub(pre_parse_regex, pre_parse_substitute, page_source)
soup = None soup = None
try: try:
soup = BeautifulSoup(page_source, 'lxml') soup = BeautifulSoup(page_source, 'lxml')

View File

@ -34,9 +34,6 @@ class CustomTab(SettingsTab):
""" """
CustomTab is the Custom settings tab in the settings dialog. CustomTab is the Custom settings tab in the settings dialog.
""" """
def __init__(self, parent, title, visible_title, icon_path):
super(CustomTab, self).__init__(parent, title, visible_title, icon_path)
def setupUi(self): def setupUi(self):
self.setObjectName('CustomTab') self.setObjectName('CustomTab')
super(CustomTab, self).setupUi() super(CustomTab, self).setupUi()

View File

@ -29,7 +29,7 @@ from openlp.core.common import Settings, translate
from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint
from openlp.plugins.images.lib import ImageMediaItem, ImageTab from openlp.plugins.images.lib import ImageMediaItem, ImageTab, upgrade
from openlp.plugins.images.lib.db import init_schema from openlp.plugins.images.lib.db import init_schema
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -50,7 +50,7 @@ class ImagePlugin(Plugin):
def __init__(self): def __init__(self):
super(ImagePlugin, self).__init__('images', __default_settings__, ImageMediaItem, ImageTab) super(ImagePlugin, self).__init__('images', __default_settings__, ImageMediaItem, ImageTab)
self.manager = Manager('images', init_schema) self.manager = Manager('images', init_schema, upgrade_mod=upgrade)
self.weight = -7 self.weight = -7
self.icon_path = ':/plugins/plugin_images.png' self.icon_path = ':/plugins/plugin_images.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)

View File

@ -22,11 +22,10 @@
""" """
The :mod:`db` module provides the database and schema that is the backend for the Images plugin. The :mod:`db` module provides the database and schema that is the backend for the Images plugin.
""" """
from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db from openlp.core.lib.db import BaseModel, PathType, init_db
class ImageGroups(BaseModel): class ImageGroups(BaseModel):
@ -65,7 +64,7 @@ def init_schema(url):
* id * id
* group_id * group_id
* filename * file_path
""" """
session, metadata = init_db(url) session, metadata = init_db(url)
@ -80,7 +79,7 @@ def init_schema(url):
image_filenames_table = Table('image_filenames', metadata, image_filenames_table = Table('image_filenames', metadata,
Column('id', types.Integer(), primary_key=True), Column('id', types.Integer(), primary_key=True),
Column('group_id', types.Integer(), ForeignKey('image_groups.id'), default=None), Column('group_id', types.Integer(), ForeignKey('image_groups.id'), default=None),
Column('filename', types.Unicode(255), nullable=False) Column('file_path', PathType(), nullable=False)
) )
mapper(ImageGroups, image_groups_table) mapper(ImageGroups, image_groups_table)

View File

@ -31,9 +31,6 @@ class ImageTab(SettingsTab):
""" """
ImageTab is the images settings tab in the settings dialog. ImageTab is the images settings tab in the settings dialog.
""" """
def __init__(self, parent, name, visible_title, icon_path):
super(ImageTab, self).__init__(parent, name, visible_title, icon_path)
def setupUi(self): def setupUi(self):
self.setObjectName('ImagesTab') self.setObjectName('ImagesTab')
super(ImageTab, self).setupUi() super(ImageTab, self).setupUi()

View File

@ -21,7 +21,6 @@
############################################################################### ###############################################################################
import logging import logging
import os
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
@ -99,11 +98,11 @@ class ImageMediaItem(MediaManagerItem):
self.list_view.setIconSize(QtCore.QSize(88, 50)) self.list_view.setIconSize(QtCore.QSize(88, 50))
self.list_view.setIndentation(self.list_view.default_indentation) self.list_view.setIndentation(self.list_view.default_indentation)
self.list_view.allow_internal_dnd = True self.list_view.allow_internal_dnd = True
self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails') self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
check_directory_exists(Path(self.service_path)) check_directory_exists(self.service_path)
# Load images from the database # Load images from the database
self.load_full_list( self.load_full_list(
self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True) self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path), initial_load=True)
def add_list_view_to_toolbar(self): def add_list_view_to_toolbar(self):
""" """
@ -211,8 +210,8 @@ class ImageMediaItem(MediaManagerItem):
""" """
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id) images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
for image in images: for image in images:
delete_file(Path(self.service_path, os.path.split(image.filename)[1])) delete_file(self.service_path / image.file_path.name)
delete_file(Path(self.generate_thumbnail_path(image))) delete_file(self.generate_thumbnail_path(image))
self.manager.delete_object(ImageFilenames, image.id) self.manager.delete_object(ImageFilenames, image.id)
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id) image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
for group in image_groups: for group in image_groups:
@ -234,8 +233,8 @@ class ImageMediaItem(MediaManagerItem):
if row_item: if row_item:
item_data = row_item.data(0, QtCore.Qt.UserRole) item_data = row_item.data(0, QtCore.Qt.UserRole)
if isinstance(item_data, ImageFilenames): if isinstance(item_data, ImageFilenames):
delete_file(Path(self.service_path, row_item.text(0))) delete_file(self.service_path / row_item.text(0))
delete_file(Path(self.generate_thumbnail_path(item_data))) delete_file(self.generate_thumbnail_path(item_data))
if item_data.group_id == 0: if item_data.group_id == 0:
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item)) self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
else: else:
@ -326,17 +325,19 @@ class ImageMediaItem(MediaManagerItem):
""" """
Generate a path to the thumbnail Generate a path to the thumbnail
:param image: An instance of ImageFileNames :param openlp.plugins.images.lib.db.ImageFilenames image: The image to generate the thumbnail path for.
:return: A path to the thumbnail of type str :return: A path to the thumbnail
:rtype: openlp.core.common.path.Path
""" """
ext = os.path.splitext(image.filename)[1].lower() ext = image.file_path.suffix.lower()
return os.path.join(self.service_path, '{}{}'.format(str(image.id), ext)) return self.service_path / '{name:d}{ext}'.format(name=image.id, ext=ext)
def load_full_list(self, images, initial_load=False, open_group=None): def load_full_list(self, images, initial_load=False, open_group=None):
""" """
Replace the list of images and groups in the interface. Replace the list of images and groups in the interface.
:param images: A List of Image Filenames objects that will be used to reload the mediamanager list. :param list[openlp.plugins.images.lib.db.ImageFilenames] images: A List of Image Filenames objects that will be
used to reload the mediamanager list.
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images. :param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images.
:param open_group: ImageGroups object of the group that must be expanded after reloading the list in the :param open_group: ImageGroups object of the group that must be expanded after reloading the list in the
interface. interface.
@ -352,34 +353,34 @@ class ImageMediaItem(MediaManagerItem):
self.expand_group(open_group.id) self.expand_group(open_group.id)
# Sort the images by its filename considering language specific. # Sort the images by its filename considering language specific.
# characters. # characters.
images.sort(key=lambda image_object: get_locale_key(os.path.split(str(image_object.filename))[1])) images.sort(key=lambda image_object: get_locale_key(image_object.file_path.name))
for image_file in images: for image in images:
log.debug('Loading image: {name}'.format(name=image_file.filename)) log.debug('Loading image: {name}'.format(name=image.file_path))
filename = os.path.split(image_file.filename)[1] file_name = image.file_path.name
thumb = self.generate_thumbnail_path(image_file) thumbnail_path = self.generate_thumbnail_path(image)
if not os.path.exists(image_file.filename): if not image.file_path.exists():
icon = build_icon(':/general/general_delete.png') icon = build_icon(':/general/general_delete.png')
else: else:
if validate_thumb(image_file.filename, thumb): if validate_thumb(image.file_path, thumbnail_path):
icon = build_icon(thumb) icon = build_icon(thumbnail_path)
else: else:
icon = create_thumb(image_file.filename, thumb) icon = create_thumb(image.file_path, thumbnail_path)
item_name = QtWidgets.QTreeWidgetItem([filename]) item_name = QtWidgets.QTreeWidgetItem([file_name])
item_name.setText(0, filename) item_name.setText(0, file_name)
item_name.setIcon(0, icon) item_name.setIcon(0, icon)
item_name.setToolTip(0, image_file.filename) item_name.setToolTip(0, str(image.file_path))
item_name.setData(0, QtCore.Qt.UserRole, image_file) item_name.setData(0, QtCore.Qt.UserRole, image)
if image_file.group_id == 0: if image.group_id == 0:
self.list_view.addTopLevelItem(item_name) self.list_view.addTopLevelItem(item_name)
else: else:
group_items[image_file.group_id].addChild(item_name) group_items[image.group_id].addChild(item_name)
if not initial_load: if not initial_load:
self.main_window.increment_progress_bar() self.main_window.increment_progress_bar()
if not initial_load: if not initial_load:
self.main_window.finished_progress_bar() self.main_window.finished_progress_bar()
self.application.set_normal_cursor() self.application.set_normal_cursor()
def validate_and_load(self, files, target_group=None): def validate_and_load(self, file_paths, target_group=None):
""" """
Process a list for files either from the File Dialog or from Drag and Drop. Process a list for files either from the File Dialog or from Drag and Drop.
This method is overloaded from MediaManagerItem. This method is overloaded from MediaManagerItem.
@ -388,15 +389,15 @@ class ImageMediaItem(MediaManagerItem):
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
""" """
self.application.set_normal_cursor() self.application.set_normal_cursor()
self.load_list(files, target_group) self.load_list(file_paths, target_group)
last_dir = os.path.split(files[0])[0] last_dir = file_paths[0].parent
Settings().setValue(self.settings_section + '/last directory', Path(last_dir)) Settings().setValue(self.settings_section + '/last directory', last_dir)
def load_list(self, images, target_group=None, initial_load=False): def load_list(self, image_paths, target_group=None, initial_load=False):
""" """
Add new images to the database. This method is called when adding images using the Add button or DnD. Add new images to the database. This method is called when adding images using the Add button or DnD.
:param images: A List of strings containing the filenames of the files to be loaded :param list[openlp.core.common.Path] image_paths: A list of file paths to the images to be loaded
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images :param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images
""" """
@ -429,7 +430,7 @@ class ImageMediaItem(MediaManagerItem):
else: else:
self.choose_group_form.existing_radio_button.setDisabled(False) self.choose_group_form.existing_radio_button.setDisabled(False)
self.choose_group_form.group_combobox.setDisabled(False) self.choose_group_form.group_combobox.setDisabled(False)
# Ask which group the images should be saved in # Ask which group the image_paths should be saved in
if self.choose_group_form.exec(selected_group=preselect_group): if self.choose_group_form.exec(selected_group=preselect_group):
if self.choose_group_form.nogroup_radio_button.isChecked(): if self.choose_group_form.nogroup_radio_button.isChecked():
# User chose 'No group' # User chose 'No group'
@ -461,33 +462,33 @@ class ImageMediaItem(MediaManagerItem):
return return
# Initialize busy cursor and progress bar # Initialize busy cursor and progress bar
self.application.set_busy_cursor() self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(images)) self.main_window.display_progress_bar(len(image_paths))
# Save the new images in the database # Save the new image_paths in the database
self.save_new_images_list(images, group_id=parent_group.id, reload_list=False) self.save_new_images_list(image_paths, group_id=parent_group.id, reload_list=False)
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path),
initial_load=initial_load, open_group=parent_group) initial_load=initial_load, open_group=parent_group)
self.application.set_normal_cursor() self.application.set_normal_cursor()
def save_new_images_list(self, images_list, group_id=0, reload_list=True): def save_new_images_list(self, image_paths, group_id=0, reload_list=True):
""" """
Convert a list of image filenames to ImageFilenames objects and save them in the database. Convert a list of image filenames to ImageFilenames objects and save them in the database.
:param images_list: A List of strings containing image filenames :param list[Path] image_paths: A List of file paths to image
:param group_id: The ID of the group to save the images in :param group_id: The ID of the group to save the images in
:param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving :param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
the new images the new images
""" """
for filename in images_list: for image_path in image_paths:
if not isinstance(filename, str): if not isinstance(image_path, Path):
continue continue
log.debug('Adding new image: {name}'.format(name=filename)) log.debug('Adding new image: {name}'.format(name=image_path))
image_file = ImageFilenames() image_file = ImageFilenames()
image_file.group_id = group_id image_file.group_id = group_id
image_file.filename = str(filename) image_file.file_path = image_path
self.manager.save_object(image_file) self.manager.save_object(image_file)
self.main_window.increment_progress_bar() self.main_window.increment_progress_bar()
if reload_list and images_list: if reload_list and image_paths:
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename)) self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path))
def dnd_move_internal(self, target): def dnd_move_internal(self, target):
""" """
@ -581,8 +582,8 @@ class ImageMediaItem(MediaManagerItem):
return False return False
# Find missing files # Find missing files
for image in images: for image in images:
if not os.path.exists(image.filename): if not image.file_path.exists():
missing_items_file_names.append(image.filename) missing_items_file_names.append(str(image.file_path))
# We cannot continue, as all images do not exist. # We cannot continue, as all images do not exist.
if not images: if not images:
if not remote: if not remote:
@ -601,9 +602,9 @@ class ImageMediaItem(MediaManagerItem):
return False return False
# Continue with the existing images. # Continue with the existing images.
for image in images: for image in images:
name = os.path.split(image.filename)[1] name = image.file_path.name
thumbnail = self.generate_thumbnail_path(image) thumbnail_path = self.generate_thumbnail_path(image)
service_item.add_from_image(image.filename, name, background, thumbnail) service_item.add_from_image(str(image.file_path), name, background, str(thumbnail_path))
return True return True
def check_group_exists(self, new_group): def check_group_exists(self, new_group):
@ -640,7 +641,7 @@ class ImageMediaItem(MediaManagerItem):
if not self.check_group_exists(new_group): if not self.check_group_exists(new_group):
if self.manager.save_object(new_group): if self.manager.save_object(new_group):
self.load_full_list(self.manager.get_all_objects( self.load_full_list(self.manager.get_all_objects(
ImageFilenames, order_by_ref=ImageFilenames.filename)) ImageFilenames, order_by_ref=ImageFilenames.file_path))
self.expand_group(new_group.id) self.expand_group(new_group.id)
self.fill_groups_combobox(self.choose_group_form.group_combobox) self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox) self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
@ -675,9 +676,9 @@ class ImageMediaItem(MediaManagerItem):
if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames): if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
# Only continue when an image is selected. # Only continue when an image is selected.
return return
filename = bitem.data(0, QtCore.Qt.UserRole).filename file_path = bitem.data(0, QtCore.Qt.UserRole).file_path
if os.path.exists(filename): if file_path.exists():
if self.live_controller.display.direct_image(filename, background): if self.live_controller.display.direct_image(str(file_path), background):
self.reset_action.setVisible(True) self.reset_action.setVisible(True)
else: else:
critical_error_message_box( critical_error_message_box(
@ -687,22 +688,22 @@ class ImageMediaItem(MediaManagerItem):
critical_error_message_box( critical_error_message_box(
UiStrings().LiveBGError, UiStrings().LiveBGError,
translate('ImagePlugin.MediaItem', 'There was a problem replacing your background, ' translate('ImagePlugin.MediaItem', 'There was a problem replacing your background, '
'the image file "{name}" no longer exists.').format(name=filename)) 'the image file "{name}" no longer exists.').format(name=file_path))
def search(self, string, show_error=True): def search(self, string, show_error=True):
""" """
Perform a search on the image file names. Perform a search on the image file names.
:param string: The glob to search for :param str string: The glob to search for
:param show_error: Unused. :param bool show_error: Unused.
""" """
files = self.manager.get_all_objects( files = self.manager.get_all_objects(
ImageFilenames, filter_clause=ImageFilenames.filename.contains(string), ImageFilenames, filter_clause=ImageFilenames.file_path.contains(string),
order_by_ref=ImageFilenames.filename) order_by_ref=ImageFilenames.file_path)
results = [] results = []
for file_object in files: for file_object in files:
filename = os.path.split(str(file_object.filename))[1] file_name = file_object.file_path.name
results.append([file_object.filename, filename]) results.append([str(file_object.file_path), file_name])
return results return results
def create_item_from_id(self, item_id): def create_item_from_id(self, item_id):
@ -711,8 +712,9 @@ class ImageMediaItem(MediaManagerItem):
:param item_id: Id to make live :param item_id: Id to make live
""" """
item_id = Path(item_id)
item = QtWidgets.QTreeWidgetItem() item = QtWidgets.QTreeWidgetItem()
item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.filename == item_id) item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.file_path == item_id)
item.setText(0, os.path.basename(item_data.filename)) item.setText(0, item_data.file_path.name)
item.setData(0, QtCore.Qt.UserRole, item_data) item.setData(0, QtCore.Qt.UserRole, item_data)
return item return item

View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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 #
###############################################################################
"""
The :mod:`upgrade` module provides the migration path for the OLP Paths database
"""
import json
import logging
from sqlalchemy import Column, Table
from openlp.core.common import AppLocation
from openlp.core.common.db import drop_columns
from openlp.core.common.json import OpenLPJsonEncoder
from openlp.core.common.path import Path
from openlp.core.lib.db import PathType, get_upgrade_op
log = logging.getLogger(__name__)
__version__ = 2
def upgrade_1(session, metadata):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
log.debug('Skipping upgrade_1 of files DB - not used')
def upgrade_2(session, metadata):
"""
Version 2 upgrade - Move file path from old db to JSON encoded path to new db. Added during 2.5 dev
"""
log.debug('Starting upgrade_2 for file_path to JSON')
old_table = Table('image_filenames', metadata, autoload=True)
if 'file_path' not in [col.name for col in old_table.c.values()]:
op = get_upgrade_op(session)
op.add_column('image_filenames', Column('file_path', PathType()))
conn = op.get_bind()
results = conn.execute('SELECT * FROM image_filenames')
data_path = AppLocation.get_data_path()
for row in results.fetchall():
file_path_json = json.dumps(Path(row.filename), cls=OpenLPJsonEncoder, base_path=data_path)
sql = 'UPDATE image_filenames SET file_path = \'{file_path_json}\' WHERE id = {id}'.format(
file_path_json=file_path_json, id=row.id)
conn.execute(sql)
# Drop old columns
if metadata.bind.url.get_dialect().name == 'sqlite':
drop_columns(op, 'image_filenames', ['filename', ])
else:
op.drop_constraint('image_filenames', 'foreignkey')
op.drop_column('image_filenames', 'filenames')

View File

@ -32,11 +32,14 @@
# http://nxsy.org/comparing-documents-with-openoffice-and-python # http://nxsy.org/comparing-documents-with-openoffice-and-python
import logging import logging
import os
import time import time
from openlp.core.common import is_win, Registry, delete_file from PyQt5 import QtCore
from openlp.core.common.path import Path
from openlp.core.common import Registry, delete_file, get_uno_command, get_uno_instance, is_win
from openlp.core.lib import ScreenList
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
TextType
if is_win(): if is_win():
from win32com.client import Dispatch from win32com.client import Dispatch
@ -55,14 +58,6 @@ else:
except ImportError: except ImportError:
uno_available = False uno_available = False
from PyQt5 import QtCore
from openlp.core.lib import ScreenList
from openlp.core.common import get_uno_command, get_uno_instance
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument, \
TextType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -203,12 +198,15 @@ class ImpressDocument(PresentationDocument):
Class which holds information and controls a single presentation. Class which holds information and controls a single presentation.
""" """
def __init__(self, controller, presentation): def __init__(self, controller, document_path):
""" """
Constructor, store information about the file and initialise. Constructor, store information about the file and initialise.
:param openlp.core.common.path.Path document_path: File path for the document to load
:rtype: None
""" """
log.debug('Init Presentation OpenOffice') log.debug('Init Presentation OpenOffice')
super(ImpressDocument, self).__init__(controller, presentation) super().__init__(controller, document_path)
self.document = None self.document = None
self.presentation = None self.presentation = None
self.control = None self.control = None
@ -225,10 +223,9 @@ class ImpressDocument(PresentationDocument):
if desktop is None: if desktop is None:
self.controller.start_process() self.controller.start_process()
desktop = self.controller.get_com_desktop() desktop = self.controller.get_com_desktop()
url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20')
else: else:
desktop = self.controller.get_uno_desktop() desktop = self.controller.get_uno_desktop()
url = uno.systemPathToFileUrl(self.file_path) url = self.file_path.as_uri()
if desktop is None: if desktop is None:
return False return False
self.desktop = desktop self.desktop = desktop
@ -254,11 +251,8 @@ class ImpressDocument(PresentationDocument):
log.debug('create thumbnails OpenOffice') log.debug('create thumbnails OpenOffice')
if self.check_thumbnails(): if self.check_thumbnails():
return return
if is_win(): temp_folder_path = self.get_temp_folder()
thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \ thumb_dir_url = temp_folder_path.as_uri()
.replace(':', '|').replace(' ', '%20')
else:
thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder())
properties = [] properties = []
properties.append(self.create_property('FilterName', 'impress_png_Export')) properties.append(self.create_property('FilterName', 'impress_png_Export'))
properties = tuple(properties) properties = tuple(properties)
@ -266,17 +260,17 @@ class ImpressDocument(PresentationDocument):
pages = doc.getDrawPages() pages = doc.getDrawPages()
if not pages: if not pages:
return return
if not os.path.isdir(self.get_temp_folder()): if not temp_folder_path.is_dir():
os.makedirs(self.get_temp_folder()) temp_folder_path.mkdir(parents=True)
for index in range(pages.getCount()): for index in range(pages.getCount()):
page = pages.getByIndex(index) page = pages.getByIndex(index)
doc.getCurrentController().setCurrentPage(page) doc.getCurrentController().setCurrentPage(page)
url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1)) url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1)
path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png') path = temp_folder_path / '{number:d}.png'.format(number=index + 1)
try: try:
doc.storeToURL(url_path, properties) doc.storeToURL(url_path, properties)
self.convert_thumbnail(path, index + 1) self.convert_thumbnail(path, index + 1)
delete_file(Path(path)) delete_file(path)
except ErrorCodeIOException as exception: except ErrorCodeIOException as exception:
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode)) log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
except: except:

View File

@ -19,15 +19,13 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import logging import logging
import os
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.common import Registry, Settings, UiStrings, translate
from openlp.core.common.languagemanager import get_locale_key from openlp.core.common.languagemanager import get_locale_key
from openlp.core.common.path import path_to_str from openlp.core.common.path import Path, path_to_str, str_to_path
from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\
build_icon, check_item_selected, create_thumb, validate_thumb build_icon, check_item_selected, create_thumb, validate_thumb
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
@ -128,7 +126,7 @@ class PresentationMediaItem(MediaManagerItem):
""" """
self.list_view.setIconSize(QtCore.QSize(88, 50)) self.list_view.setIconSize(QtCore.QSize(88, 50))
file_paths = Settings().value(self.settings_section + '/presentations files') file_paths = Settings().value(self.settings_section + '/presentations files')
self.load_list([path_to_str(file) for file in file_paths], initial_load=True) self.load_list([path_to_str(path) for path in file_paths], initial_load=True)
self.populate_display_types() self.populate_display_types()
def populate_display_types(self): def populate_display_types(self):
@ -152,54 +150,57 @@ class PresentationMediaItem(MediaManagerItem):
else: else:
self.presentation_widget.hide() self.presentation_widget.hide()
def load_list(self, files, target_group=None, initial_load=False): def load_list(self, file_paths, target_group=None, initial_load=False):
""" """
Add presentations into the media manager. This is called both on initial load of the plugin to populate with Add presentations into the media manager. This is called both on initial load of the plugin to populate with
existing files, and when the user adds new files via the media manager. existing files, and when the user adds new files via the media manager.
:param list[openlp.core.common.path.Path] file_paths: List of file paths to add to the media manager.
""" """
current_list = self.get_file_list() file_paths = [str_to_path(filename) for filename in file_paths]
titles = [file_path.name for file_path in current_list] current_paths = self.get_file_list()
titles = [file_path.name for file_path in current_paths]
self.application.set_busy_cursor() self.application.set_busy_cursor()
if not initial_load: if not initial_load:
self.main_window.display_progress_bar(len(files)) self.main_window.display_progress_bar(len(file_paths))
# Sort the presentations by its filename considering language specific characters. # Sort the presentations by its filename considering language specific characters.
files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) file_paths.sort(key=lambda file_path: get_locale_key(file_path.name))
for file in files: for file_path in file_paths:
if not initial_load: if not initial_load:
self.main_window.increment_progress_bar() self.main_window.increment_progress_bar()
if current_list.count(file) > 0: if current_paths.count(file_path) > 0:
continue continue
filename = os.path.split(file)[1] file_name = file_path.name
if not os.path.exists(file): if not file_path.exists():
item_name = QtWidgets.QListWidgetItem(filename) item_name = QtWidgets.QListWidgetItem(file_name)
item_name.setIcon(build_icon(ERROR_IMAGE)) item_name.setIcon(build_icon(ERROR_IMAGE))
item_name.setData(QtCore.Qt.UserRole, file) item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path))
item_name.setToolTip(file) item_name.setToolTip(str(file_path))
self.list_view.addItem(item_name) self.list_view.addItem(item_name)
else: else:
if titles.count(filename) > 0: if titles.count(file_name) > 0:
if not initial_load: if not initial_load:
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'), critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
translate('PresentationPlugin.MediaItem', translate('PresentationPlugin.MediaItem',
'A presentation with that filename already exists.')) 'A presentation with that filename already exists.'))
continue continue
controller_name = self.find_controller_by_type(filename) controller_name = self.find_controller_by_type(file_path)
if controller_name: if controller_name:
controller = self.controllers[controller_name] controller = self.controllers[controller_name]
doc = controller.add_document(file) doc = controller.add_document(file_path)
thumb = os.path.join(doc.get_thumbnail_folder(), 'icon.png') thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
preview = doc.get_thumbnail_path(1, True) preview_path = doc.get_thumbnail_path(1, True)
if not preview and not initial_load: if not preview_path and not initial_load:
doc.load_presentation() doc.load_presentation()
preview = doc.get_thumbnail_path(1, True) preview_path = doc.get_thumbnail_path(1, True)
doc.close_presentation() doc.close_presentation()
if not (preview and os.path.exists(preview)): if not (preview_path and preview_path.exists()):
icon = build_icon(':/general/general_delete.png') icon = build_icon(':/general/general_delete.png')
else: else:
if validate_thumb(preview, thumb): if validate_thumb(Path(preview_path), Path(thumbnail_path)):
icon = build_icon(thumb) icon = build_icon(thumbnail_path)
else: else:
icon = create_thumb(preview, thumb) icon = create_thumb(str(preview_path), str(thumbnail_path))
else: else:
if initial_load: if initial_load:
icon = build_icon(':/general/general_delete.png') icon = build_icon(':/general/general_delete.png')
@ -208,10 +209,10 @@ class PresentationMediaItem(MediaManagerItem):
translate('PresentationPlugin.MediaItem', translate('PresentationPlugin.MediaItem',
'This type of presentation is not supported.')) 'This type of presentation is not supported.'))
continue continue
item_name = QtWidgets.QListWidgetItem(filename) item_name = QtWidgets.QListWidgetItem(file_name)
item_name.setData(QtCore.Qt.UserRole, file) item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path))
item_name.setIcon(icon) item_name.setIcon(icon)
item_name.setToolTip(file) item_name.setToolTip(str(file_path))
self.list_view.addItem(item_name) self.list_view.addItem(item_name)
if not initial_load: if not initial_load:
self.main_window.finished_progress_bar() self.main_window.finished_progress_bar()
@ -228,8 +229,8 @@ class PresentationMediaItem(MediaManagerItem):
self.application.set_busy_cursor() self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(row_list)) self.main_window.display_progress_bar(len(row_list))
for item in items: for item in items:
filepath = str(item.data(QtCore.Qt.UserRole)) file_path = str_to_path(item.data(QtCore.Qt.UserRole))
self.clean_up_thumbnails(filepath) self.clean_up_thumbnails(file_path)
self.main_window.increment_progress_bar() self.main_window.increment_progress_bar()
self.main_window.finished_progress_bar() self.main_window.finished_progress_bar()
for row in row_list: for row in row_list:
@ -237,30 +238,29 @@ class PresentationMediaItem(MediaManagerItem):
Settings().setValue(self.settings_section + '/presentations files', self.get_file_list()) Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
self.application.set_normal_cursor() self.application.set_normal_cursor()
def clean_up_thumbnails(self, filepath, clean_for_update=False): def clean_up_thumbnails(self, file_path, clean_for_update=False):
""" """
Clean up the files created such as thumbnails Clean up the files created such as thumbnails
:param filepath: File path of the presention to clean up after :param openlp.core.common.path.Path file_path: File path of the presention to clean up after
:param clean_for_update: Only clean thumbnails if update is needed :param bool clean_for_update: Only clean thumbnails if update is needed
:return: None :rtype: None
""" """
for cidx in self.controllers: for cidx in self.controllers:
root, file_ext = os.path.splitext(filepath) file_ext = file_path.suffix[1:]
file_ext = file_ext[1:]
if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports: if file_ext in self.controllers[cidx].supports or file_ext in self.controllers[cidx].also_supports:
doc = self.controllers[cidx].add_document(filepath) doc = self.controllers[cidx].add_document(file_path)
if clean_for_update: if clean_for_update:
thumb_path = doc.get_thumbnail_path(1, True) thumb_path = doc.get_thumbnail_path(1, True)
if not thumb_path or not os.path.exists(filepath) or os.path.getmtime( if not thumb_path or not file_path.exists() or \
thumb_path) < os.path.getmtime(filepath): thumb_path.stat().st_mtime < file_path.stat().st_mtime:
doc.presentation_deleted() doc.presentation_deleted()
else: else:
doc.presentation_deleted() doc.presentation_deleted()
doc.close_presentation() doc.close_presentation()
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False, def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
context=ServiceItemContext.Service, presentation_file=None): context=ServiceItemContext.Service, file_path=None):
""" """
Generate the slide data. Needs to be implemented by the plugin. Generate the slide data. Needs to be implemented by the plugin.
@ -276,10 +276,9 @@ class PresentationMediaItem(MediaManagerItem):
items = self.list_view.selectedItems() items = self.list_view.selectedItems()
if len(items) > 1: if len(items) > 1:
return False return False
filename = presentation_file if file_path is None:
if filename is None: file_path = str_to_path(items[0].data(QtCore.Qt.UserRole))
filename = items[0].data(QtCore.Qt.UserRole) file_type = file_path.suffix.lower()[1:]
file_type = os.path.splitext(filename.lower())[1][1:]
if not self.display_type_combo_box.currentText(): if not self.display_type_combo_box.currentText():
return False return False
service_item.add_capability(ItemCapabilities.CanEditTitle) service_item.add_capability(ItemCapabilities.CanEditTitle)
@ -292,29 +291,28 @@ class PresentationMediaItem(MediaManagerItem):
# force a nonexistent theme # force a nonexistent theme
service_item.theme = -1 service_item.theme = -1
for bitem in items: for bitem in items:
filename = presentation_file if file_path is None:
if filename is None: file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
filename = bitem.data(QtCore.Qt.UserRole) path, file_name = file_path.parent, file_path.name
(path, name) = os.path.split(filename) service_item.title = file_name
service_item.title = name if file_path.exists():
if os.path.exists(filename): processor = self.find_controller_by_type(file_path)
processor = self.find_controller_by_type(filename)
if not processor: if not processor:
return False return False
controller = self.controllers[processor] controller = self.controllers[processor]
service_item.processor = None service_item.processor = None
doc = controller.add_document(filename) doc = controller.add_document(file_path)
if doc.get_thumbnail_path(1, True) is None or not os.path.isfile( if doc.get_thumbnail_path(1, True) is None or \
os.path.join(doc.get_temp_folder(), 'mainslide001.png')): not (doc.get_temp_folder() / 'mainslide001.png').is_file():
doc.load_presentation() doc.load_presentation()
i = 1 i = 1
image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
while os.path.isfile(image): while image_path.is_file():
service_item.add_from_image(image, name, thumbnail=thumbnail) service_item.add_from_image(str(image_path), file_name, thumbnail=str(thumbnail_path))
i += 1 i += 1
image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i)) image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide{number:d}.png'.format(number=i)) thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
service_item.add_capability(ItemCapabilities.HasThumbnails) service_item.add_capability(ItemCapabilities.HasThumbnails)
doc.close_presentation() doc.close_presentation()
return True return True
@ -324,34 +322,34 @@ class PresentationMediaItem(MediaManagerItem):
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
translate('PresentationPlugin.MediaItem', translate('PresentationPlugin.MediaItem',
'The presentation {name} no longer exists.' 'The presentation {name} no longer exists.'
).format(name=filename)) ).format(name=file_path))
return False return False
else: else:
service_item.processor = self.display_type_combo_box.currentText() service_item.processor = self.display_type_combo_box.currentText()
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay) service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
for bitem in items: for bitem in items:
filename = bitem.data(QtCore.Qt.UserRole) file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
(path, name) = os.path.split(filename) path, file_name = file_path.parent, file_path.name
service_item.title = name service_item.title = file_name
if os.path.exists(filename): if file_path.exists():
if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic': if self.display_type_combo_box.itemData(self.display_type_combo_box.currentIndex()) == 'automatic':
service_item.processor = self.find_controller_by_type(filename) service_item.processor = self.find_controller_by_type(file_path)
if not service_item.processor: if not service_item.processor:
return False return False
controller = self.controllers[service_item.processor] controller = self.controllers[service_item.processor]
doc = controller.add_document(filename) doc = controller.add_document(file_path)
if doc.get_thumbnail_path(1, True) is None: if doc.get_thumbnail_path(1, True) is None:
doc.load_presentation() doc.load_presentation()
i = 1 i = 1
img = doc.get_thumbnail_path(i, True) thumbnail_path = doc.get_thumbnail_path(i, True)
if img: if thumbnail_path:
# Get titles and notes # Get titles and notes
titles, notes = doc.get_titles_and_notes() titles, notes = doc.get_titles_and_notes()
service_item.add_capability(ItemCapabilities.HasDisplayTitle) service_item.add_capability(ItemCapabilities.HasDisplayTitle)
if notes.count('') != len(notes): if notes.count('') != len(notes):
service_item.add_capability(ItemCapabilities.HasNotes) service_item.add_capability(ItemCapabilities.HasNotes)
service_item.add_capability(ItemCapabilities.HasThumbnails) service_item.add_capability(ItemCapabilities.HasThumbnails)
while img: while thumbnail_path:
# Use title and note if available # Use title and note if available
title = '' title = ''
if titles and len(titles) >= i: if titles and len(titles) >= i:
@ -359,9 +357,9 @@ class PresentationMediaItem(MediaManagerItem):
note = '' note = ''
if notes and len(notes) >= i: if notes and len(notes) >= i:
note = notes[i - 1] note = notes[i - 1]
service_item.add_from_command(path, name, img, title, note) service_item.add_from_command(str(path), file_name, str(thumbnail_path), title, note)
i += 1 i += 1
img = doc.get_thumbnail_path(i, True) thumbnail_path = doc.get_thumbnail_path(i, True)
doc.close_presentation() doc.close_presentation()
return True return True
else: else:
@ -371,7 +369,7 @@ class PresentationMediaItem(MediaManagerItem):
'Missing Presentation'), 'Missing Presentation'),
translate('PresentationPlugin.MediaItem', translate('PresentationPlugin.MediaItem',
'The presentation {name} is incomplete, ' 'The presentation {name} is incomplete, '
'please reload.').format(name=filename)) 'please reload.').format(name=file_path))
return False return False
else: else:
# File is no longer present # File is no longer present
@ -379,18 +377,20 @@ class PresentationMediaItem(MediaManagerItem):
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'), critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
translate('PresentationPlugin.MediaItem', translate('PresentationPlugin.MediaItem',
'The presentation {name} no longer exists.' 'The presentation {name} no longer exists.'
).format(name=filename)) ).format(name=file_path))
return False return False
def find_controller_by_type(self, filename): def find_controller_by_type(self, file_path):
""" """
Determine the default application controller to use for the selected file type. This is used if "Automatic" is Determine the default application controller to use for the selected file type. This is used if "Automatic" is
set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension. set as the preferred controller. Find the first (alphabetic) enabled controller which "supports" the extension.
If none found, then look for a controller which "also supports" it instead. If none found, then look for a controller which "also supports" it instead.
:param filename: The file name :param openlp.core.common.path.Path file_path: The file path
:return: The default application controller for this file type, or None if not supported
:rtype: PresentationController
""" """
file_type = os.path.splitext(filename)[1][1:] file_type = file_path.suffix[1:]
if not file_type: if not file_type:
return None return None
for controller in self.controllers: for controller in self.controllers:

View File

@ -19,16 +19,15 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import logging
import copy import copy
import os import logging
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Registry, Settings from openlp.core.common import Registry, Settings
from openlp.core.ui import HideMode from openlp.core.common.path import Path
from openlp.core.lib import ServiceItemContext from openlp.core.lib import ServiceItemContext
from openlp.core.ui import HideMode
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -325,21 +324,25 @@ class MessageListener(object):
is_live = message[1] is_live = message[1]
item = message[0] item = message[0]
hide_mode = message[2] hide_mode = message[2]
file = item.get_frame_path() file_path = Path(item.get_frame_path())
self.handler = item.processor self.handler = item.processor
# When starting presentation from the servicemanager we convert # When starting presentation from the servicemanager we convert
# PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager # PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager
# the conversion has already been done at this point. # the conversion has already been done at this point.
file_type = os.path.splitext(file.lower())[1][1:] file_type = file_path.suffix.lower()[1:]
if file_type in PDF_CONTROLLER_FILETYPES: if file_type in PDF_CONTROLLER_FILETYPES:
log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file)) log.debug('Converting from pdf/xps/oxps to images for serviceitem with file {name}'.format(name=file_path))
# Create a copy of the original item, and then clear the original item so it can be filled with images # 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_cpy = copy.copy(item)
item.__init__(None) item.__init__(None)
if is_live: if is_live:
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live, file) # TODO: To Path object
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Live,
str(file_path))
else: else:
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview, file) # TODO: To Path object
self.media_item.generate_slide_data(item, item_cpy, False, False, ServiceItemContext.Preview,
str(file_path))
# Some of the original serviceitem attributes is needed in the new serviceitem # Some of the original serviceitem attributes is needed in the new serviceitem
item.footer = item_cpy.footer item.footer = item_cpy.footer
item.from_service = item_cpy.from_service item.from_service = item_cpy.from_service
@ -352,13 +355,13 @@ class MessageListener(object):
self.handler = None self.handler = None
else: else:
if self.handler == self.media_item.automatic: if self.handler == self.media_item.automatic:
self.handler = self.media_item.find_controller_by_type(file) self.handler = self.media_item.find_controller_by_type(file_path)
if not self.handler: if not self.handler:
return return
else: else:
# the saved handler is not present so need to use one based on file suffix. # the saved handler is not present so need to use one based on file_path suffix.
if not self.controllers[self.handler].available: if not self.controllers[self.handler].available:
self.handler = self.media_item.find_controller_by_type(file) self.handler = self.media_item.find_controller_by_type(file_path)
if not self.handler: if not self.handler:
return return
if is_live: if is_live:
@ -370,7 +373,7 @@ class MessageListener(object):
if self.handler is None: if self.handler is None:
self.controller = controller self.controller = controller
else: else:
controller.add_handler(self.controllers[self.handler], file, hide_mode, message[3]) controller.add_handler(self.controllers[self.handler], file_path, hide_mode, message[3])
self.timer.start() self.timer.start()
def slide(self, message): def slide(self, message):

View File

@ -23,12 +23,11 @@
import os import os
import logging import logging
import re import re
from shutil import which
from subprocess import check_output, CalledProcessError from subprocess import check_output, CalledProcessError
from openlp.core.common import AppLocation, check_binary_exists from openlp.core.common import AppLocation, check_binary_exists
from openlp.core.common import Settings, is_win from openlp.core.common import Settings, is_win
from openlp.core.common.path import Path, path_to_str from openlp.core.common.path import which
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
@ -66,11 +65,12 @@ class PdfController(PresentationController):
Function that checks whether a binary is either ghostscript or mudraw or neither. Function that checks whether a binary is either ghostscript or mudraw or neither.
Is also used from presentationtab.py Is also used from presentationtab.py
:param program_path:The full path to the binary to check. :param openlp.core.common.path.Path program_path: The full path to the binary to check.
:return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid. :return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
:rtype: str | None
""" """
program_type = None program_type = None
runlog = check_binary_exists(Path(program_path)) runlog = check_binary_exists(program_path)
# Analyse the output to see it the program is mudraw, ghostscript or neither # Analyse the output to see it the program is mudraw, ghostscript or neither
for line in runlog.splitlines(): for line in runlog.splitlines():
decoded_line = line.decode() decoded_line = line.decode()
@ -107,30 +107,29 @@ class PdfController(PresentationController):
:return: True if program to open PDF-files was found, otherwise False. :return: True if program to open PDF-files was found, otherwise False.
""" """
log.debug('check_installed Pdf') log.debug('check_installed Pdf')
self.mudrawbin = '' self.mudrawbin = None
self.mutoolbin = '' self.mutoolbin = None
self.gsbin = '' self.gsbin = None
self.also_supports = [] self.also_supports = []
# Use the user defined program if given # Use the user defined program if given
if Settings().value('presentations/enable_pdf_program'): if Settings().value('presentations/enable_pdf_program'):
pdf_program = path_to_str(Settings().value('presentations/pdf_program')) program_path = Settings().value('presentations/pdf_program')
program_type = self.process_check_binary(pdf_program) program_type = self.process_check_binary(program_path)
if program_type == 'gs': if program_type == 'gs':
self.gsbin = pdf_program self.gsbin = program_path
elif program_type == 'mudraw': elif program_type == 'mudraw':
self.mudrawbin = pdf_program self.mudrawbin = program_path
elif program_type == 'mutool': elif program_type == 'mutool':
self.mutoolbin = pdf_program self.mutoolbin = program_path
else: else:
# Fallback to autodetection # Fallback to autodetection
application_path = str(AppLocation.get_directory(AppLocation.AppDir)) application_path = AppLocation.get_directory(AppLocation.AppDir)
if is_win(): if is_win():
# for windows we only accept mudraw.exe or mutool.exe in the base folder # for windows we only accept mudraw.exe or mutool.exe in the base folder
application_path = str(AppLocation.get_directory(AppLocation.AppDir)) if (application_path / 'mudraw.exe').is_file():
if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): self.mudrawbin = application_path / 'mudraw.exe'
self.mudrawbin = os.path.join(application_path, 'mudraw.exe') elif (application_path / 'mutool.exe').is_file():
elif os.path.isfile(os.path.join(application_path, 'mutool.exe')): self.mutoolbin = application_path / 'mutool.exe'
self.mutoolbin = os.path.join(application_path, 'mutool.exe')
else: else:
DEVNULL = open(os.devnull, 'wb') DEVNULL = open(os.devnull, 'wb')
# First try to find mudraw # First try to find mudraw
@ -143,11 +142,11 @@ class PdfController(PresentationController):
self.gsbin = which('gs') self.gsbin = which('gs')
# Last option: check if mudraw or mutool is placed in OpenLP base folder # Last option: check if mudraw or mutool is placed in OpenLP base folder
if not self.mudrawbin and not self.mutoolbin and not self.gsbin: if not self.mudrawbin and not self.mutoolbin and not self.gsbin:
application_path = str(AppLocation.get_directory(AppLocation.AppDir)) application_path = AppLocation.get_directory(AppLocation.AppDir)
if os.path.isfile(os.path.join(application_path, 'mudraw')): if (application_path / 'mudraw').is_file():
self.mudrawbin = os.path.join(application_path, 'mudraw') self.mudrawbin = application_path / 'mudraw'
elif os.path.isfile(os.path.join(application_path, 'mutool')): elif (application_path / 'mutool').is_file():
self.mutoolbin = os.path.join(application_path, 'mutool') self.mutoolbin = application_path / 'mutool'
if self.mudrawbin or self.mutoolbin: if self.mudrawbin or self.mutoolbin:
self.also_supports = ['xps', 'oxps'] self.also_supports = ['xps', 'oxps']
return True return True
@ -172,12 +171,15 @@ class PdfDocument(PresentationDocument):
image-serviceitem on the fly and present as such. Therefore some of the 'playback' image-serviceitem on the fly and present as such. Therefore some of the 'playback'
functions is not implemented. functions is not implemented.
""" """
def __init__(self, controller, presentation): def __init__(self, controller, document_path):
""" """
Constructor, store information about the file and initialise. Constructor, store information about the file and initialise.
:param openlp.core.common.path.Path document_path: Path to the document to load
:rtype: None
""" """
log.debug('Init Presentation Pdf') log.debug('Init Presentation Pdf')
PresentationDocument.__init__(self, controller, presentation) super().__init__(controller, document_path)
self.presentation = None self.presentation = None
self.blanked = False self.blanked = False
self.hidden = False self.hidden = False
@ -200,13 +202,13 @@ class PdfDocument(PresentationDocument):
:return: The resolution dpi to be used. :return: The resolution dpi to be used.
""" """
# Use a postscript script to get size of the pdf. It is assumed that all pages have same size # Use a postscript script to get size of the pdf. It is assumed that all pages have same size
gs_resolution_script = str(AppLocation.get_directory( gs_resolution_script = AppLocation.get_directory(
AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps' AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps'
# Run the script on the pdf to get the size # Run the script on the pdf to get the size
runlog = [] runlog = []
try: try:
runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH', runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
'-sFile=' + self.file_path, gs_resolution_script], '-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)],
startupinfo=self.startupinfo) startupinfo=self.startupinfo)
except CalledProcessError as e: except CalledProcessError as e:
log.debug(' '.join(e.cmd)) log.debug(' '.join(e.cmd))
@ -240,46 +242,47 @@ class PdfDocument(PresentationDocument):
:return: True is loading succeeded, otherwise False. :return: True is loading succeeded, otherwise False.
""" """
log.debug('load_presentation pdf') log.debug('load_presentation pdf')
temp_dir_path = self.get_temp_folder()
# Check if the images has already been created, and if yes load them # Check if the images has already been created, and if yes load them
if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')): if (temp_dir_path / 'mainslide001.png').is_file():
created_files = sorted(os.listdir(self.get_temp_folder())) created_files = sorted(temp_dir_path.glob('*'))
for fn in created_files: for image_path in created_files:
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): if image_path.is_file():
self.image_files.append(os.path.join(self.get_temp_folder(), fn)) self.image_files.append(image_path)
self.num_pages = len(self.image_files) self.num_pages = len(self.image_files)
return True return True
size = ScreenList().current['size'] size = ScreenList().current['size']
# Generate images from PDF that will fit the frame. # Generate images from PDF that will fit the frame.
runlog = '' runlog = ''
try: try:
if not os.path.isdir(self.get_temp_folder()): if not temp_dir_path.is_dir():
os.makedirs(self.get_temp_folder()) temp_dir_path.mkdir(parents=True)
# The %03d in the file name is handled by each binary # The %03d in the file name is handled by each binary
if self.controller.mudrawbin: if self.controller.mudrawbin:
log.debug('loading presentation using mudraw') log.debug('loading presentation using mudraw')
runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()), runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()),
'-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], '-h', str(size.height()),
'-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)],
startupinfo=self.startupinfo) startupinfo=self.startupinfo)
elif self.controller.mutoolbin: elif self.controller.mutoolbin:
log.debug('loading presentation using mutool') log.debug('loading presentation using mutool')
runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h', runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()),
str(size.height()), '-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'),
'-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path], str(self.file_path)],
startupinfo=self.startupinfo) startupinfo=self.startupinfo)
elif self.controller.gsbin: elif self.controller.gsbin:
log.debug('loading presentation using gs') log.debug('loading presentation using gs')
resolution = self.gs_get_resolution(size) resolution = self.gs_get_resolution(size)
runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m', runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
'-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
'-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), '-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'),
self.file_path], startupinfo=self.startupinfo) str(self.file_path)], startupinfo=self.startupinfo)
created_files = sorted(os.listdir(self.get_temp_folder())) created_files = sorted(temp_dir_path.glob('*'))
for fn in created_files: for image_path in created_files:
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)): if image_path.is_file():
self.image_files.append(os.path.join(self.get_temp_folder(), fn)) self.image_files.append(image_path)
except Exception as e: except Exception as e:
log.debug(e) log.exception(runlog)
log.debug(runlog)
return False return False
self.num_pages = len(self.image_files) self.num_pages = len(self.image_files)
# Create thumbnails # Create thumbnails

View File

@ -120,15 +120,16 @@ class PowerpointDocument(PresentationDocument):
Class which holds information and controls a single presentation. Class which holds information and controls a single presentation.
""" """
def __init__(self, controller, presentation): def __init__(self, controller, document_path):
""" """
Constructor, store information about the file and initialise. Constructor, store information about the file and initialise.
:param controller: :param controller:
:param presentation: :param openlp.core.common.path.Path document_path: Path to the document to load
:rtype: None
""" """
log.debug('Init Presentation Powerpoint') log.debug('Init Presentation Powerpoint')
super(PowerpointDocument, self).__init__(controller, presentation) super().__init__(controller, document_path)
self.presentation = None self.presentation = None
self.index_map = {} self.index_map = {}
self.slide_count = 0 self.slide_count = 0
@ -145,7 +146,7 @@ class PowerpointDocument(PresentationDocument):
try: try:
if not self.controller.process: if not self.controller.process:
self.controller.start_process() self.controller.start_process()
self.controller.process.Presentations.Open(os.path.normpath(self.file_path), False, False, False) self.controller.process.Presentations.Open(str(self.file_path), False, False, False)
self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count)
self.create_thumbnails() self.create_thumbnails()
self.create_titles_and_notes() self.create_titles_and_notes()
@ -177,7 +178,7 @@ class PowerpointDocument(PresentationDocument):
if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden: if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden:
self.index_map[key] = num + 1 self.index_map[key] = num + 1
self.presentation.Slides(num + 1).Export( self.presentation.Slides(num + 1).Export(
os.path.join(self.get_thumbnail_folder(), 'slide{key:d}.png'.format(key=key)), 'png', 320, 240) str(self.get_thumbnail_folder() / 'slide{key:d}.png'.format(key=key)), 'png', 320, 240)
key += 1 key += 1
self.slide_count = key - 1 self.slide_count = key - 1
@ -363,9 +364,8 @@ class PowerpointDocument(PresentationDocument):
width=size.width(), width=size.width(),
horizontal=(right - left))) horizontal=(right - left)))
log.debug('window title: {title}'.format(title=window_title)) log.debug('window title: {title}'.format(title=window_title))
filename_root, filename_ext = os.path.splitext(os.path.basename(self.file_path))
if size.y() == top and size.height() == (bottom - top) and size.x() == left and \ if size.y() == top and size.height() == (bottom - top) and size.x() == left and \
size.width() == (right - left) and filename_root in window_title: size.width() == (right - left) and self.file_path.stem in window_title:
log.debug('Found a match and will save the handle') log.debug('Found a match and will save the handle')
self.presentation_hwnd = hwnd self.presentation_hwnd = hwnd
# Stop powerpoint from flashing in the taskbar # Stop powerpoint from flashing in the taskbar

View File

@ -85,9 +85,9 @@ class PptviewController(PresentationController):
if self.process: if self.process:
return return
log.debug('start PPTView') log.debug('start PPTView')
dll_path = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), dll_path = AppLocation.get_directory(AppLocation.AppDir) \
'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll') / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll'
self.process = cdll.LoadLibrary(dll_path) self.process = cdll.LoadLibrary(str(dll_path))
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
self.process.SetDebug(1) self.process.SetDebug(1)
@ -104,12 +104,15 @@ class PptviewDocument(PresentationDocument):
""" """
Class which holds information and controls a single presentation. Class which holds information and controls a single presentation.
""" """
def __init__(self, controller, presentation): def __init__(self, controller, document_path):
""" """
Constructor, store information about the file and initialise. Constructor, store information about the file and initialise.
:param openlp.core.common.path.Path document_path: File path to the document to load
:rtype: None
""" """
log.debug('Init Presentation PowerPoint') log.debug('Init Presentation PowerPoint')
super(PptviewDocument, self).__init__(controller, presentation) super().__init__(controller, document_path)
self.presentation = None self.presentation = None
self.ppt_id = None self.ppt_id = None
self.blanked = False self.blanked = False
@ -121,17 +124,16 @@ class PptviewDocument(PresentationDocument):
the background PptView task started earlier. the background PptView task started earlier.
""" """
log.debug('LoadPresentation') log.debug('LoadPresentation')
temp_folder = self.get_temp_folder() temp_path = self.get_temp_folder()
size = ScreenList().current['size'] size = ScreenList().current['size']
rect = RECT(size.x(), size.y(), size.right(), size.bottom()) rect = RECT(size.x(), size.y(), size.right(), size.bottom())
self.file_path = os.path.normpath(self.file_path) preview_path = temp_path / 'slide'
preview_path = os.path.join(temp_folder, 'slide')
# Ensure that the paths are null terminated # Ensure that the paths are null terminated
byte_file_path = self.file_path.encode('utf-16-le') + b'\0' file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0'
preview_path = preview_path.encode('utf-16-le') + b'\0' preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0'
if not os.path.isdir(temp_folder): if not temp_path.is_dir():
os.makedirs(temp_folder) temp_path.mkdir(parents=True)
self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path) self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16)
if self.ppt_id >= 0: if self.ppt_id >= 0:
self.create_thumbnails() self.create_thumbnails()
self.stop_presentation() self.stop_presentation()
@ -148,7 +150,7 @@ class PptviewDocument(PresentationDocument):
return return
log.debug('create_thumbnails proceeding') log.debug('create_thumbnails proceeding')
for idx in range(self.get_slide_count()): for idx in range(self.get_slide_count()):
path = '{folder}\\slide{index}.bmp'.format(folder=self.get_temp_folder(), index=str(idx + 1)) path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=idx + 1)
self.convert_thumbnail(path, idx + 1) self.convert_thumbnail(path, idx + 1)
def create_titles_and_notes(self): def create_titles_and_notes(self):
@ -161,13 +163,12 @@ class PptviewDocument(PresentationDocument):
""" """
titles = None titles = None
notes = None notes = None
filename = os.path.normpath(self.file_path)
# let's make sure we have a valid zipped presentation # let's make sure we have a valid zipped presentation
if os.path.exists(filename) and zipfile.is_zipfile(filename): if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)):
namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"} "a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
# open the file # open the file
with zipfile.ZipFile(filename) as zip_file: with zipfile.ZipFile(str(self.file_path)) as zip_file:
# find the presentation.xml to get the slide count # find the presentation.xml to get the slide count
with zip_file.open('ppt/presentation.xml') as pres: with zip_file.open('ppt/presentation.xml') as pres:
tree = ElementTree.parse(pres) tree = ElementTree.parse(pres)

View File

@ -19,15 +19,12 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import logging import logging
import os
import shutil
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, md5_hash
from openlp.core.common.path import Path from openlp.core.common.path import Path, rmtree
from openlp.core.lib import create_thumb, validate_thumb from openlp.core.lib import create_thumb, validate_thumb
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -86,20 +83,27 @@ class PresentationDocument(object):
Returns a path to an image containing a preview for the requested slide Returns a path to an image containing a preview for the requested slide
""" """
def __init__(self, controller, name): def __init__(self, controller, document_path):
""" """
Constructor for the PresentationController class Constructor for the PresentationController class
:param controller:
:param openlp.core.common.path.Path document_path: Path to the document to load.
:rtype: None
""" """
self.controller = controller self.controller = controller
self._setup(name) self._setup(document_path)
def _setup(self, name): def _setup(self, document_path):
""" """
Run some initial setup. This method is separate from __init__ in order to mock it out in tests. Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
:param openlp.core.common.path.Path document_path: Path to the document to load.
:rtype: None
""" """
self.slide_number = 0 self.slide_number = 0
self.file_path = name self.file_path = document_path
check_directory_exists(Path(self.get_thumbnail_folder())) check_directory_exists(self.get_thumbnail_folder())
def load_presentation(self): def load_presentation(self):
""" """
@ -116,49 +120,54 @@ class PresentationDocument(object):
a file, e.g. thumbnails a file, e.g. thumbnails
""" """
try: try:
if os.path.exists(self.get_thumbnail_folder()): thumbnail_folder_path = self.get_thumbnail_folder()
shutil.rmtree(self.get_thumbnail_folder()) temp_folder_path = self.get_temp_folder()
if os.path.exists(self.get_temp_folder()): if thumbnail_folder_path.exists():
shutil.rmtree(self.get_temp_folder()) rmtree(thumbnail_folder_path)
if temp_folder_path.exists():
rmtree(temp_folder_path)
except OSError: except OSError:
log.exception('Failed to delete presentation controller files') log.exception('Failed to delete presentation controller files')
def get_file_name(self):
"""
Return just the filename of the presentation, without the directory
"""
return os.path.split(self.file_path)[1]
def get_thumbnail_folder(self): def get_thumbnail_folder(self):
""" """
The location where thumbnail images will be stored The location where thumbnail images will be stored
:return: The path to the thumbnail
:rtype: openlp.core.common.path.Path
""" """
# TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
if Settings().value('presentations/thumbnail_scheme') == 'md5': if Settings().value('presentations/thumbnail_scheme') == 'md5':
folder = md5_hash(self.file_path.encode('utf-8')) folder = md5_hash(bytes(self.file_path))
else: else:
folder = self.get_file_name() folder = self.file_path.name
return os.path.join(self.controller.thumbnail_folder, folder) return Path(self.controller.thumbnail_folder, folder)
def get_temp_folder(self): def get_temp_folder(self):
""" """
The location where thumbnail images will be stored The location where thumbnail images will be stored
:return: The path to the temporary file folder
:rtype: openlp.core.common.path.Path
""" """
# TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed # TODO: If statement can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
if Settings().value('presentations/thumbnail_scheme') == 'md5': if Settings().value('presentations/thumbnail_scheme') == 'md5':
folder = md5_hash(self.file_path.encode('utf-8')) folder = md5_hash(bytes(self.file_path))
else: else:
folder = folder = self.get_file_name() folder = self.file_path.name
return os.path.join(self.controller.temp_folder, folder) return Path(self.controller.temp_folder, folder)
def check_thumbnails(self): def check_thumbnails(self):
""" """
Returns ``True`` if the thumbnail images exist and are more recent than the powerpoint file. Check that the last thumbnail image exists and is valid and are more recent than the powerpoint file.
:return: If the thumbnail is valid
:rtype: bool
""" """
last_image = self.get_thumbnail_path(self.get_slide_count(), True) last_image_path = self.get_thumbnail_path(self.get_slide_count(), True)
if not (last_image and os.path.isfile(last_image)): if not (last_image_path and last_image_path.is_file()):
return False return False
return validate_thumb(self.file_path, last_image) return validate_thumb(Path(self.file_path), Path(last_image_path))
def close_presentation(self): def close_presentation(self):
""" """
@ -241,25 +250,31 @@ class PresentationDocument(object):
""" """
pass pass
def convert_thumbnail(self, file, idx): def convert_thumbnail(self, image_path, index):
""" """
Convert the slide image the application made to a scaled 360px height .png image. Convert the slide image the application made to a scaled 360px height .png image.
:param openlp.core.common.path.Path image_path: Path to the image to create a thumb nail of
:param int index: The index of the slide to create the thumbnail for.
:rtype: None
""" """
if self.check_thumbnails(): if self.check_thumbnails():
return return
if os.path.isfile(file): if image_path.is_file():
thumb_path = self.get_thumbnail_path(idx, False) thumb_path = self.get_thumbnail_path(index, False)
create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360)) create_thumb(str(image_path), str(thumb_path), False, QtCore.QSize(-1, 360))
def get_thumbnail_path(self, slide_no, check_exists): def get_thumbnail_path(self, slide_no, check_exists=False):
""" """
Returns an image path containing a preview for the requested slide Returns an image path containing a preview for the requested slide
:param slide_no: The slide an image is required for, starting at 1 :param int slide_no: The slide an image is required for, starting at 1
:param check_exists: :param bool check_exists: Check if the generated path exists
:return: The path, or None if the :param:`check_exists` is True and the file does not exist
:rtype: openlp.core.common.path.Path | None
""" """
path = os.path.join(self.get_thumbnail_folder(), self.controller.thumbnail_prefix + str(slide_no) + '.png') path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png')
if os.path.isfile(path) or not check_exists: if path.is_file() or not check_exists:
return path return path
else: else:
return None return None
@ -302,44 +317,38 @@ class PresentationDocument(object):
Reads the titles from the titles file and Reads the titles from the titles file and
the notes files and returns the content in two lists the notes files and returns the content in two lists
""" """
titles = []
notes = [] notes = []
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') titles_path = self.get_thumbnail_folder() / 'titles.txt'
if os.path.exists(titles_file): try:
try: titles = titles_path.read_text().splitlines()
with open(titles_file, encoding='utf-8') as fi: except:
titles = fi.read().splitlines() log.exception('Failed to open/read existing titles file')
except: titles = []
log.exception('Failed to open/read existing titles file')
titles = []
for slide_no, title in enumerate(titles, 1): for slide_no, title in enumerate(titles, 1):
notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes{number:d}.txt'.format(number=slide_no)) notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
note = '' try:
if os.path.exists(notes_file): note = notes_path.read_text()
try: except:
with open(notes_file, encoding='utf-8') as fn: log.exception('Failed to open/read notes file')
note = fn.read() note = ''
except:
log.exception('Failed to open/read notes file')
note = ''
notes.append(note) notes.append(note)
return titles, notes return titles, notes
def save_titles_and_notes(self, titles, notes): def save_titles_and_notes(self, titles, notes):
""" """
Performs the actual persisting of titles to the titles.txt Performs the actual persisting of titles to the titles.txt and notes to the slideNote%.txt
and notes to the slideNote%.txt
:param list[str] titles: The titles to save
:param list[str] notes: The notes to save
:rtype: None
""" """
if titles: if titles:
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') titles_path = self.get_thumbnail_folder() / 'titles.txt'
with open(titles_file, mode='wt', encoding='utf-8') as fo: titles_path.write_text('\n'.join(titles))
fo.writelines(titles)
if notes: if notes:
for slide_no, note in enumerate(notes, 1): for slide_no, note in enumerate(notes, 1):
notes_file = os.path.join(self.get_thumbnail_folder(), notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
'slideNotes{number:d}.txt'.format(number=slide_no)) notes_path.write_text(note)
with open(notes_file, mode='wt', encoding='utf-8') as fn:
fn.write(note)
class PresentationController(object): class PresentationController(object):
@ -416,12 +425,11 @@ class PresentationController(object):
self.document_class = document_class self.document_class = document_class
self.settings_section = self.plugin.settings_section self.settings_section = self.plugin.settings_section
self.available = None self.available = None
self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name) self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name
self.thumbnail_folder = os.path.join( self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
self.thumbnail_prefix = 'slide' self.thumbnail_prefix = 'slide'
check_directory_exists(Path(self.thumbnail_folder)) check_directory_exists(self.thumbnail_folder)
check_directory_exists(Path(self.temp_folder)) check_directory_exists(self.temp_folder)
def enabled(self): def enabled(self):
""" """
@ -456,11 +464,15 @@ class PresentationController(object):
log.debug('Kill') log.debug('Kill')
self.close_presentation() self.close_presentation()
def add_document(self, name): def add_document(self, document_path):
""" """
Called when a new presentation document is opened. Called when a new presentation document is opened.
:param openlp.core.common.path.Path document_path: Path to the document to load
:return: The document
:rtype: PresentationDocument
""" """
document = self.document_class(self, name) document = self.document_class(self, document_path)
self.docs.append(document) self.docs.append(document)
return document return document

View File

@ -38,7 +38,6 @@ class PresentationTab(SettingsTab):
""" """
Constructor Constructor
""" """
self.parent = parent
self.controllers = controllers self.controllers = controllers
super(PresentationTab, self).__init__(parent, title, visible_title, icon_path) super(PresentationTab, self).__init__(parent, title, visible_title, icon_path)
self.activated = False self.activated = False
@ -194,7 +193,7 @@ class PresentationTab(SettingsTab):
pdf_program_path = self.program_path_edit.path pdf_program_path = self.program_path_edit.path
enable_pdf_program = self.pdf_program_check_box.checkState() enable_pdf_program = self.pdf_program_check_box.checkState()
# If the given program is blank disable using the program # If the given program is blank disable using the program
if not pdf_program_path: if pdf_program_path is None:
enable_pdf_program = 0 enable_pdf_program = 0
if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'): if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'):
Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path) Settings().setValue(self.settings_section + '/pdf_program', pdf_program_path)
@ -220,9 +219,11 @@ class PresentationTab(SettingsTab):
def on_program_path_edit_path_changed(self, new_path): def on_program_path_edit_path_changed(self, new_path):
""" """
Select the mudraw or ghostscript binary that should be used. Handle the `pathEditChanged` signal from program_path_edit
:param openlp.core.common.path.Path new_path: File path to the new program
:rtype: None
""" """
new_path = path_to_str(new_path)
if new_path: if new_path:
if not PdfController.process_check_binary(new_path): if not PdfController.process_check_binary(new_path):
critical_error_message_box(UiStrings().Error, critical_error_message_box(UiStrings().Error,

View File

@ -19,10 +19,11 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
Download and "install" the remote web client
"""
import os import os
import zipfile from zipfile import ZipFile
import urllib.error
from openlp.core.common import AppLocation, Registry from openlp.core.common import AppLocation, Registry
from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size
@ -38,7 +39,7 @@ def deploy_zipfile(app_root, zip_name):
:return: None :return: None
""" """
zip_file = os.path.join(app_root, zip_name) zip_file = os.path.join(app_root, zip_name)
web_zip = zipfile.ZipFile(zip_file) web_zip = ZipFile(zip_file)
web_zip.extractall(app_root) web_zip.extractall(app_root)
@ -48,11 +49,10 @@ def download_sha256():
""" """
user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
try: try:
web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'), web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent})
header=('User-Agent', user_agent)) except ConnectionError:
except (urllib.error.URLError, ConnectionError) as err:
return False return False
file_bits = web_config.read().decode('utf-8').split() file_bits = web_config.split()
return file_bits[0], file_bits[2] return file_bits[0], file_bits[2]
@ -64,6 +64,6 @@ def download_and_check(callback=None):
file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
callback.setRange(0, file_size) callback.setRange(0, file_size)
if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'), if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'),
os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'), AppLocation.get_section_data_path('remotes') / 'site.zip',
sha256=sha256): sha256=sha256):
deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip') deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip')

View File

@ -62,7 +62,7 @@ import re
from lxml import etree, objectify from lxml import etree, objectify
from openlp.core.common import translate, Settings from openlp.core.common import translate, Settings
from openlp.core.common.versionchecker import get_application_version from openlp.core.version import get_version
from openlp.core.lib import FormattingTags from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic
@ -234,7 +234,7 @@ class OpenLyrics(object):
# Append the necessary meta data to the song. # Append the necessary meta data to the song.
song_xml.set('xmlns', NAMESPACE) song_xml.set('xmlns', NAMESPACE)
song_xml.set('version', OpenLyrics.IMPLEMENTED_VERSION) song_xml.set('version', OpenLyrics.IMPLEMENTED_VERSION)
application_name = 'OpenLP ' + get_application_version()['version'] application_name = 'OpenLP ' + get_version()['version']
song_xml.set('createdIn', application_name) song_xml.set('createdIn', application_name)
song_xml.set('modifiedIn', application_name) song_xml.set('modifiedIn', application_name)
# "Convert" 2012-08-27 11:49:15 to 2012-08-27T11:49:15. # "Convert" 2012-08-27 11:49:15 to 2012-08-27T11:49:15.

View File

@ -25,10 +25,10 @@ The :mod:`db` module provides the ability to provide a csv file of all songs
import csv import csv
import logging import logging
from PyQt5 import QtWidgets
from openlp.core.common import Registry, translate from openlp.core.common import Registry, translate
from openlp.core.common.path import Path
from openlp.core.lib.ui import critical_error_message_box from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.lib.filedialog import FileDialog
from openlp.plugins.songs.lib.db import Song from openlp.plugins.songs.lib.db import Song
@ -42,58 +42,55 @@ def report_song_list():
""" """
main_window = Registry().get('main_window') main_window = Registry().get('main_window')
plugin = Registry().get('songs').plugin plugin = Registry().get('songs').plugin
report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName( report_file_path, filter_used = FileDialog.getSaveFileName(
main_window, main_window,
translate('SongPlugin.ReportSongList', 'Save File'), translate('SongPlugin.ReportSongList', 'Save File'),
translate('SongPlugin.ReportSongList', 'song_extract.csv'), Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')),
translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) translate('SongPlugin.ReportSongList', 'CSV format (*.csv)'))
if not report_file_name: if report_file_path is None:
main_window.error_message( main_window.error_message(
translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), translate('SongPlugin.ReportSongList', 'Output Path Not Selected'),
translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your ' translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n'
'report. \nPlease select an existing path ' 'Please select an existing path on your computer.')
'on your computer.')
) )
return return
if not report_file_name.endswith('csv'): report_file_path.with_suffix('.csv')
report_file_name += '.csv'
file_handle = None
Registry().get('application').set_busy_cursor() Registry().get('application').set_busy_cursor()
try: try:
file_handle = open(report_file_name, 'wt') with report_file_path.open('wt') as file_handle:
fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
headers = dict((n, n) for n in fieldnames) headers = dict((n, n) for n in fieldnames)
writer.writerow(headers) writer.writerow(headers)
song_list = plugin.manager.get_all_objects(Song) song_list = plugin.manager.get_all_objects(Song)
for song in song_list: for song in song_list:
author_list = [] author_list = []
for author_song in song.authors_songs: for author_song in song.authors_songs:
author_list.append(author_song.author.display_name) author_list.append(author_song.author.display_name)
author_string = ' | '.join(author_list) author_string = ' | '.join(author_list)
book_list = [] book_list = []
for book_song in song.songbook_entries: for book_song in song.songbook_entries:
if hasattr(book_song, 'entry') and book_song.entry: if hasattr(book_song, 'entry') and book_song.entry:
book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry)) book_list.append('{name} #{entry}'.format(name=book_song.songbook.name, entry=book_song.entry))
book_string = ' | '.join(book_list) book_string = ' | '.join(book_list)
topic_list = [] topic_list = []
for topic_song in song.topics: for topic_song in song.topics:
if hasattr(topic_song, 'name'): if hasattr(topic_song, 'name'):
topic_list.append(topic_song.name) topic_list.append(topic_song.name)
topic_string = ' | '.join(topic_list) topic_string = ' | '.join(topic_list)
writer.writerow({'Title': song.title, writer.writerow({'Title': song.title,
'Alternative Title': song.alternate_title, 'Alternative Title': song.alternate_title,
'Copyright': song.copyright, 'Copyright': song.copyright,
'Author(s)': author_string, 'Author(s)': author_string,
'Song Book': book_string, 'Song Book': book_string,
'Topic': topic_string}) 'Topic': topic_string})
Registry().get('application').set_normal_cursor() Registry().get('application').set_normal_cursor()
main_window.information_message( main_window.information_message(
translate('SongPlugin.ReportSongList', 'Report Creation'), translate('SongPlugin.ReportSongList', 'Report Creation'),
translate('SongPlugin.ReportSongList', translate('SongPlugin.ReportSongList',
'Report \n{name} \nhas been successfully created. ').format(name=report_file_name) 'Report \n{name} \nhas been successfully created. ').format(name=report_file_path)
) )
except OSError as ose: except OSError as ose:
Registry().get('application').set_normal_cursor() Registry().get('application').set_normal_cursor()
log.exception('Failed to write out song usage records') log.exception('Failed to write out song usage records')
@ -101,6 +98,3 @@ def report_song_list():
translate('SongPlugin.ReportSongList', translate('SongPlugin.ReportSongList',
'An error occurred while extracting: {error}' 'An error occurred while extracting: {error}'
).format(error=ose.strerror)) ).format(error=ose.strerror))
finally:
if file_handle:
file_handle.close()

View File

@ -19,7 +19,6 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core.common import translate from openlp.core.common import translate

View File

@ -19,7 +19,6 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import logging import logging
import os import os
@ -60,7 +59,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
def on_report_path_edit_path_changed(self, file_path): def on_report_path_edit_path_changed(self, file_path):
""" """
Called when the path in the `PathEdit` has changed Handle the `pathEditChanged` signal from report_path_edit
:param openlp.core.common.path.Path file_path: The new path. :param openlp.core.common.path.Path file_path: The new path.
:rtype: None :rtype: None
@ -72,7 +71,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
Ok was triggered so lets save the data and run the report Ok was triggered so lets save the data and run the report
""" """
log.debug('accept') log.debug('accept')
path = path_to_str(self.report_path_edit.path) path = self.report_path_edit.path
if not path: if not path:
self.main_window.error_message( self.main_window.error_message(
translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'), translate('SongUsagePlugin.SongUsageDetailForm', 'Output Path Not Selected'),
@ -80,7 +79,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
' song usage report. \nPlease select an existing path on your computer.') ' song usage report. \nPlease select an existing path on your computer.')
) )
return return
check_directory_exists(Path(path)) check_directory_exists(path)
file_name = translate('SongUsagePlugin.SongUsageDetailForm', file_name = translate('SongUsagePlugin.SongUsageDetailForm',
'usage_detail_{old}_{new}.txt' 'usage_detail_{old}_{new}.txt'
).format(old=self.from_date_calendar.selectedDate().toString('ddMMyyyy'), ).format(old=self.from_date_calendar.selectedDate().toString('ddMMyyyy'),
@ -91,29 +90,25 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
SongUsageItem, and_(SongUsageItem.usagedate >= self.from_date_calendar.selectedDate().toPyDate(), SongUsageItem, and_(SongUsageItem.usagedate >= self.from_date_calendar.selectedDate().toPyDate(),
SongUsageItem.usagedate < self.to_date_calendar.selectedDate().toPyDate()), SongUsageItem.usagedate < self.to_date_calendar.selectedDate().toPyDate()),
[SongUsageItem.usagedate, SongUsageItem.usagetime]) [SongUsageItem.usagedate, SongUsageItem.usagetime])
report_file_name = os.path.join(path, file_name) report_file_name = path / file_name
file_handle = None
try: try:
file_handle = open(report_file_name, 'wb') with report_file_name.open('wb') as file_handle:
for instance in usage: for instance in usage:
record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",' record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",'
'\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime, '\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime,
title=instance.title, copyright=instance.copyright, title=instance.title, copyright=instance.copyright,
ccli=instance.ccl_number, authors=instance.authors, ccli=instance.ccl_number, authors=instance.authors,
name=instance.plugin_name, source=instance.source) name=instance.plugin_name, source=instance.source)
file_handle.write(record.encode('utf-8')) file_handle.write(record.encode('utf-8'))
self.main_window.information_message( self.main_window.information_message(
translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'), translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
translate('SongUsagePlugin.SongUsageDetailForm', 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: except OSError as ose:
log.exception('Failed to write out song usage records') log.exception('Failed to write out song usage records')
critical_error_message_box(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation Failed'), critical_error_message_box(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation Failed'),
translate('SongUsagePlugin.SongUsageDetailForm', translate('SongUsagePlugin.SongUsageDetailForm',
'An error occurred while creating the report: {error}' 'An error occurred while creating the report: {error}'
).format(error=ose.strerror)) ).format(error=ose.strerror))
finally:
if file_handle:
file_handle.close()
self.close() self.close()

View File

@ -12,7 +12,7 @@ environment:
install: install:
# Install dependencies from pypi # Install dependencies from pypi
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32 pyenchant websockets asyncio waitress six webob" - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32 pyenchant websockets asyncio waitress six webob requests"
# Install mysql dependency # Install mysql dependency
- "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df" - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df"
# Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/)

View File

@ -26,7 +26,7 @@ This script is used to check dependencies of OpenLP. It checks availability
of required python modules and their version. To verify availability of Python of required python modules and their version. To verify availability of Python
modules, simply run this script:: modules, simply run this script::
@:~$ ./check_dependencies.py $ ./check_dependencies.py
""" """
import os import os
@ -45,7 +45,7 @@ IS_MAC = sys.platform.startswith('dar')
VERS = { VERS = {
'Python': '3.0', 'Python': '3.4',
'PyQt5': '5.0', 'PyQt5': '5.0',
'Qt5': '5.0', 'Qt5': '5.0',
'sqlalchemy': '0.5', 'sqlalchemy': '0.5',
@ -97,7 +97,8 @@ MODULES = [
'asyncio', 'asyncio',
'waitress', 'waitress',
'six', 'six',
'webob' 'webob',
'requests'
] ]

View File

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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.version package.
"""
import sys
from datetime import date
from unittest.mock import MagicMock, patch
from requests.exceptions import ConnectionError
from openlp.core.version import VersionWorker, check_for_update, get_version, update_check_date
def test_worker_init():
"""Test the VersionWorker constructor"""
# GIVEN: A last check date and a current version
last_check_date = '1970-01-01'
current_version = '2.0'
# WHEN: A worker is created
worker = VersionWorker(last_check_date, current_version)
# THEN: The correct attributes should have been set
assert worker.last_check_date == last_check_date
assert worker.current_version == current_version
@patch('openlp.core.version.platform')
@patch('openlp.core.version.requests')
def test_worker_start(mock_requests, mock_platform):
"""Test the VersionWorkder.start() method"""
# GIVEN: A last check date, current version, and an instance of worker
last_check_date = '1970-01-01'
current_version = {'full': '2.0', 'version': '2.0', 'build': None}
mock_platform.system.return_value = 'Linux'
mock_platform.release.return_value = '4.12.0-1-amd64'
mock_requests.get.return_value = MagicMock(text='2.4.6')
worker = VersionWorker(last_check_date, current_version)
# WHEN: The worker is run
with patch.object(worker, 'new_version') as mock_new_version, \
patch.object(worker, 'quit') as mock_quit:
worker.start()
# THEN: The check completes and the signal is emitted
expected_download_url = 'http://www.openlp.org/files/version.txt'
expected_headers = {'User-Agent': 'OpenLP/2.0 Linux/4.12.0-1-amd64; '}
mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers)
mock_new_version.emit.assert_called_once_with('2.4.6')
mock_quit.emit.assert_called_once_with()
@patch('openlp.core.version.platform')
@patch('openlp.core.version.requests')
def test_worker_start_dev_version(mock_requests, mock_platform):
"""Test the VersionWorkder.start() method for dev versions"""
# GIVEN: A last check date, current version, and an instance of worker
last_check_date = '1970-01-01'
current_version = {'full': '2.1.3', 'version': '2.1.3', 'build': None}
mock_platform.system.return_value = 'Linux'
mock_platform.release.return_value = '4.12.0-1-amd64'
mock_requests.get.return_value = MagicMock(text='2.4.6')
worker = VersionWorker(last_check_date, current_version)
# WHEN: The worker is run
with patch.object(worker, 'new_version') as mock_new_version, \
patch.object(worker, 'quit') as mock_quit:
worker.start()
# THEN: The check completes and the signal is emitted
expected_download_url = 'http://www.openlp.org/files/dev_version.txt'
expected_headers = {'User-Agent': 'OpenLP/2.1.3 Linux/4.12.0-1-amd64; '}
mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers)
mock_new_version.emit.assert_called_once_with('2.4.6')
mock_quit.emit.assert_called_once_with()
@patch('openlp.core.version.platform')
@patch('openlp.core.version.requests')
def test_worker_start_nightly_version(mock_requests, mock_platform):
"""Test the VersionWorkder.start() method for nightlies"""
# GIVEN: A last check date, current version, and an instance of worker
last_check_date = '1970-01-01'
current_version = {'full': '2.1-bzr2345', 'version': '2.1', 'build': '2345'}
mock_platform.system.return_value = 'Linux'
mock_platform.release.return_value = '4.12.0-1-amd64'
mock_requests.get.return_value = MagicMock(text='2.4.6')
worker = VersionWorker(last_check_date, current_version)
# WHEN: The worker is run
with patch.object(worker, 'new_version') as mock_new_version, \
patch.object(worker, 'quit') as mock_quit:
worker.start()
# THEN: The check completes and the signal is emitted
expected_download_url = 'http://www.openlp.org/files/nightly_version.txt'
expected_headers = {'User-Agent': 'OpenLP/2.1-bzr2345 Linux/4.12.0-1-amd64; '}
mock_requests.get.assert_called_once_with(expected_download_url, headers=expected_headers)
mock_new_version.emit.assert_called_once_with('2.4.6')
mock_quit.emit.assert_called_once_with()
@patch('openlp.core.version.platform')
@patch('openlp.core.version.requests')
def test_worker_start_connection_error(mock_requests, mock_platform):
"""Test the VersionWorkder.start() method when a ConnectionError happens"""
# GIVEN: A last check date, current version, and an instance of worker
last_check_date = '1970-01-01'
current_version = {'full': '2.0', 'version': '2.0', 'build': None}
mock_platform.system.return_value = 'Linux'
mock_platform.release.return_value = '4.12.0-1-amd64'
mock_requests.get.side_effect = ConnectionError('Could not connect')
worker = VersionWorker(last_check_date, current_version)
# WHEN: The worker is run
with patch.object(worker, 'no_internet') as mocked_no_internet, \
patch.object(worker, 'quit') as mocked_quit:
worker.start()
# THEN: The check completes and the signal is emitted
expected_download_url = 'http://www.openlp.org/files/version.txt'
expected_headers = {'User-Agent': 'OpenLP/2.0 Linux/4.12.0-1-amd64; '}
mock_requests.get.assert_called_with(expected_download_url, headers=expected_headers)
assert mock_requests.get.call_count == 3
mocked_no_internet.emit.assert_called_once_with()
mocked_quit.emit.assert_called_once_with()
@patch('openlp.core.version.Settings')
def test_update_check_date(MockSettings):
"""Test that the update_check_date() function writes the correct date"""
# GIVEN: A mocked Settings object
mocked_settings = MagicMock()
MockSettings.return_value = mocked_settings
# WHEN: update_check_date() is called
update_check_date()
# THEN: The correct date should have been saved
mocked_settings.setValue.assert_called_once_with('core/last version test', date.today().strftime('%Y-%m-%d'))
@patch('openlp.core.version.Settings')
@patch('openlp.core.version.run_thread')
def test_check_for_update(mocked_run_thread, MockSettings):
"""Test the check_for_update() function"""
# GIVEN: A mocked settings object
mocked_settings = MagicMock()
mocked_settings.value.return_value = '1970-01-01'
MockSettings.return_value = mocked_settings
# WHEN: check_for_update() is called
check_for_update(MagicMock())
# THEN: The right things should have been called and a thread set in motion
assert mocked_run_thread.call_count == 1
@patch('openlp.core.version.Settings')
@patch('openlp.core.version.run_thread')
def test_check_for_update_skipped(mocked_run_thread, MockSettings):
"""Test that the check_for_update() function skips running if it already ran today"""
# GIVEN: A mocked settings object
mocked_settings = MagicMock()
mocked_settings.value.return_value = date.today().strftime('%Y-%m-%d')
MockSettings.return_value = mocked_settings
# WHEN: check_for_update() is called
check_for_update(MagicMock())
# THEN: The right things should have been called and a thread set in motion
assert mocked_run_thread.call_count == 0
def test_get_version_dev_version():
"""Test the get_version() function"""
# GIVEN: We're in dev mode
with patch.object(sys, 'argv', ['--dev-version']), \
patch('openlp.core.version.APPLICATION_VERSION', None):
# WHEN: get_version() is run
version = get_version()
# THEN: version is something
assert version

View File

@ -70,7 +70,7 @@ class TestWSServer(TestCase, TestMixin):
""" """
# GIVEN: A new httpserver # GIVEN: A new httpserver
# WHEN: I start the server # WHEN: I start the server
server = WebSocketServer() WebSocketServer()
# THEN: the api environment should have been created # THEN: the api environment should have been created
self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once') self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once')
@ -93,7 +93,7 @@ class TestWSServer(TestCase, TestMixin):
""" """
Test the poll function returns the correct JSON Test the poll function returns the correct JSON
""" """
# WHEN: the system is configured with a set of data # GIVEN: the system is configured with a set of data
mocked_service_manager = MagicMock() mocked_service_manager = MagicMock()
mocked_service_manager.service_id = 21 mocked_service_manager.service_id = 21
mocked_live_controller = MagicMock() mocked_live_controller = MagicMock()
@ -105,8 +105,15 @@ class TestWSServer(TestCase, TestMixin):
mocked_live_controller.desktop_screen.isChecked.return_value = False mocked_live_controller.desktop_screen.isChecked.return_value = False
Registry().register('live_controller', mocked_live_controller) Registry().register('live_controller', mocked_live_controller)
Registry().register('service_manager', mocked_service_manager) Registry().register('service_manager', mocked_service_manager)
# WHEN: The poller polls
with patch.object(self.poll, 'is_stage_active') as mocked_is_stage_active, \
patch.object(self.poll, 'is_live_active') as mocked_is_live_active, \
patch.object(self.poll, 'is_chords_active') as mocked_is_chords_active:
mocked_is_stage_active.return_value = True
mocked_is_live_active.return_value = True
mocked_is_chords_active.return_value = True
poll_json = self.poll.poll()
# THEN: the live json should be generated and match expected results # THEN: the live json should be generated and match expected results
poll_json = self.poll.poll()
self.assertTrue(poll_json['results']['blank'], 'The blank return value should be True') self.assertTrue(poll_json['results']['blank'], 'The blank return value should be True')
self.assertFalse(poll_json['results']['theme'], 'The theme return value should be False') self.assertFalse(poll_json['results']['theme'], 'The theme return value should be False')
self.assertFalse(poll_json['results']['display'], 'The display return value should be False') self.assertFalse(poll_json['results']['display'], 'The display return value should be False')

View File

@ -24,11 +24,11 @@ Functional tests to test the AppLocation class and related methods.
""" """
import os import os
import tempfile import tempfile
import socket
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file
from openlp.core.common.path import Path
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -67,7 +67,7 @@ class TestHttpUtils(TestCase, TestMixin):
""" """
with patch('openlp.core.common.httputils.sys') as mocked_sys: with patch('openlp.core.common.httputils.sys') as mocked_sys:
# GIVEN: The system is Linux # GIVEN: The system is Windows
mocked_sys.platform = 'win32' mocked_sys.platform = 'win32'
# WHEN: We call get_user_agent() # WHEN: We call get_user_agent()
@ -82,7 +82,7 @@ class TestHttpUtils(TestCase, TestMixin):
""" """
with patch('openlp.core.common.httputils.sys') as mocked_sys: with patch('openlp.core.common.httputils.sys') as mocked_sys:
# GIVEN: The system is Linux # GIVEN: The system is macOS
mocked_sys.platform = 'darwin' mocked_sys.platform = 'darwin'
# WHEN: We call get_user_agent() # WHEN: We call get_user_agent()
@ -97,7 +97,7 @@ class TestHttpUtils(TestCase, TestMixin):
""" """
with patch('openlp.core.common.httputils.sys') as mocked_sys: with patch('openlp.core.common.httputils.sys') as mocked_sys:
# GIVEN: The system is Linux # GIVEN: The system is something else
mocked_sys.platform = 'freebsd' mocked_sys.platform = 'freebsd'
# WHEN: We call get_user_agent() # WHEN: We call get_user_agent()
@ -119,182 +119,125 @@ class TestHttpUtils(TestCase, TestMixin):
# THEN: None should be returned # THEN: None should be returned
self.assertIsNone(result, 'The return value of get_web_page should be None') self.assertIsNone(result, 'The return value of get_web_page should be None')
def test_get_web_page(self): @patch('openlp.core.common.httputils.requests')
@patch('openlp.core.common.httputils.get_user_agent')
@patch('openlp.core.common.httputils.Registry')
def test_get_web_page(self, MockRegistry, mocked_get_user_agent, mocked_requests):
""" """
Test that the get_web_page method works correctly Test that the get_web_page method works correctly
""" """
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ # GIVEN: Mocked out objects and a fake URL
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ mocked_requests.get.return_value = MagicMock(text='text')
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \ mocked_get_user_agent.return_value = 'user_agent'
patch('openlp.core.common.Registry') as MockRegistry: fake_url = 'this://is.a.fake/url'
# GIVEN: Mocked out objects and a fake URL
mocked_request_object = MagicMock()
MockRequest.return_value = mocked_request_object
mocked_page_object = MagicMock()
mock_urlopen.return_value = mocked_page_object
mock_get_user_agent.return_value = 'user_agent'
fake_url = 'this://is.a.fake/url'
# WHEN: The get_web_page() method is called # WHEN: The get_web_page() method is called
returned_page = get_web_page(fake_url) returned_page = get_web_page(fake_url)
# THEN: The correct methods are called with the correct arguments and a web page is returned # THEN: The correct methods are called with the correct arguments and a web page is returned
MockRequest.assert_called_with(fake_url) mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'},
mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') proxies=None, timeout=30.0)
self.assertEqual(1, mocked_request_object.add_header.call_count, mocked_get_user_agent.assert_called_once_with()
'There should only be 1 call to add_header') assert MockRegistry.call_count == 0, 'The Registry() object should have never been called'
mock_get_user_agent.assert_called_with() assert returned_page == 'text', 'The returned page should be the mock object'
mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
mocked_page_object.geturl.assert_called_with()
self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called')
self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')
def test_get_web_page_with_header(self): @patch('openlp.core.common.httputils.requests')
@patch('openlp.core.common.httputils.get_user_agent')
def test_get_web_page_with_header(self, mocked_get_user_agent, mocked_requests):
""" """
Test that adding a header to the call to get_web_page() adds the header to the request Test that adding a header to the call to get_web_page() adds the header to the request
""" """
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ # GIVEN: Mocked out objects, a fake URL and a fake header
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ mocked_requests.get.return_value = MagicMock(text='text')
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: mocked_get_user_agent.return_value = 'user_agent'
# GIVEN: Mocked out objects, a fake URL and a fake header fake_url = 'this://is.a.fake/url'
mocked_request_object = MagicMock() fake_headers = {'Fake-Header': 'fake value'}
MockRequest.return_value = mocked_request_object
mocked_page_object = MagicMock()
mock_urlopen.return_value = mocked_page_object
mock_get_user_agent.return_value = 'user_agent'
fake_url = 'this://is.a.fake/url'
fake_header = ('Fake-Header', 'fake value')
# WHEN: The get_web_page() method is called # WHEN: The get_web_page() method is called
returned_page = get_web_page(fake_url, header=fake_header) returned_page = get_web_page(fake_url, headers=fake_headers)
# THEN: The correct methods are called with the correct arguments and a web page is returned # THEN: The correct methods are called with the correct arguments and a web page is returned
MockRequest.assert_called_with(fake_url) expected_headers = dict(fake_headers)
mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1]) expected_headers.update({'User-Agent': 'user_agent'})
self.assertEqual(2, mocked_request_object.add_header.call_count, mocked_requests.get.assert_called_once_with(fake_url, headers=expected_headers,
'There should only be 2 calls to add_header') proxies=None, timeout=30.0)
mock_get_user_agent.assert_called_with() mocked_get_user_agent.assert_called_with()
mock_urlopen.assert_called_with(mocked_request_object, timeout=30) assert returned_page == 'text', 'The returned page should be the mock object'
mocked_page_object.geturl.assert_called_with()
self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')
def test_get_web_page_with_user_agent_in_headers(self): @patch('openlp.core.common.httputils.requests')
@patch('openlp.core.common.httputils.get_user_agent')
def test_get_web_page_with_user_agent_in_headers(self, mocked_get_user_agent, mocked_requests):
""" """
Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request
""" """
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ # GIVEN: Mocked out objects, a fake URL and a fake header
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ mocked_requests.get.return_value = MagicMock(text='text')
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: fake_url = 'this://is.a.fake/url'
# GIVEN: Mocked out objects, a fake URL and a fake header user_agent_headers = {'User-Agent': 'OpenLP/2.2.0'}
mocked_request_object = MagicMock()
MockRequest.return_value = mocked_request_object
mocked_page_object = MagicMock()
mock_urlopen.return_value = mocked_page_object
fake_url = 'this://is.a.fake/url'
user_agent_header = ('User-Agent', 'OpenLP/2.2.0')
# WHEN: The get_web_page() method is called # WHEN: The get_web_page() method is called
returned_page = get_web_page(fake_url, header=user_agent_header) returned_page = get_web_page(fake_url, headers=user_agent_headers)
# THEN: The correct methods are called with the correct arguments and a web page is returned # THEN: The correct methods are called with the correct arguments and a web page is returned
MockRequest.assert_called_with(fake_url) mocked_requests.get.assert_called_once_with(fake_url, headers=user_agent_headers,
mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1]) proxies=None, timeout=30.0)
self.assertEqual(1, mocked_request_object.add_header.call_count, assert mocked_get_user_agent.call_count == 0, 'get_user_agent() should not have been called'
'There should only be 1 call to add_header') assert returned_page == 'text', 'The returned page should be "test"'
self.assertEqual(0, mock_get_user_agent.call_count, 'get_user_agent should not have been called')
mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
mocked_page_object.geturl.assert_called_with()
self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')
def test_get_web_page_update_openlp(self): @patch('openlp.core.common.httputils.requests')
@patch('openlp.core.common.httputils.get_user_agent')
@patch('openlp.core.common.httputils.Registry')
def test_get_web_page_update_openlp(self, MockRegistry, mocked_get_user_agent, mocked_requests):
""" """
Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events()
""" """
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ # GIVEN: Mocked out objects, a fake URL
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ mocked_requests.get.return_value = MagicMock(text='text')
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \ mocked_get_user_agent.return_value = 'user_agent'
patch('openlp.core.common.httputils.Registry') as MockRegistry: mocked_registry_object = MagicMock()
# GIVEN: Mocked out objects, a fake URL mocked_application_object = MagicMock()
mocked_request_object = MagicMock() mocked_registry_object.get.return_value = mocked_application_object
MockRequest.return_value = mocked_request_object MockRegistry.return_value = mocked_registry_object
mocked_page_object = MagicMock() fake_url = 'this://is.a.fake/url'
mock_urlopen.return_value = mocked_page_object
mock_get_user_agent.return_value = 'user_agent'
mocked_registry_object = MagicMock()
mocked_application_object = MagicMock()
mocked_registry_object.get.return_value = mocked_application_object
MockRegistry.return_value = mocked_registry_object
fake_url = 'this://is.a.fake/url'
# WHEN: The get_web_page() method is called # WHEN: The get_web_page() method is called
returned_page = get_web_page(fake_url, update_openlp=True) returned_page = get_web_page(fake_url, update_openlp=True)
# THEN: The correct methods are called with the correct arguments and a web page is returned # THEN: The correct methods are called with the correct arguments and a web page is returned
MockRequest.assert_called_with(fake_url) mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'},
mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') proxies=None, timeout=30.0)
self.assertEqual(1, mocked_request_object.add_header.call_count, mocked_get_user_agent.assert_called_once_with()
'There should only be 1 call to add_header') mocked_registry_object.get.assert_called_with('application')
mock_urlopen.assert_called_with(mocked_request_object, timeout=30) mocked_application_object.process_events.assert_called_with()
mocked_page_object.geturl.assert_called_with() assert returned_page == 'text', 'The returned page should be the mock object'
mocked_registry_object.get.assert_called_with('application')
mocked_application_object.process_events.assert_called_with()
self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')
def test_get_url_file_size(self): @patch('openlp.core.common.httputils.requests')
def test_get_url_file_size(self, mocked_requests):
""" """
Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() Test that calling "get_url_file_size" works correctly
""" """
with patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ # GIVEN: Mocked out objects, a fake URL
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: mocked_requests.head.return_value = MagicMock(headers={'Content-Length': 100})
# GIVEN: Mocked out objects, a fake URL fake_url = 'this://is.a.fake/url'
mocked_page_object = MagicMock()
mock_urlopen.return_value = mocked_page_object
mock_get_user_agent.return_value = 'user_agent'
fake_url = 'this://is.a.fake/url'
# WHEN: The get_url_file_size() method is called # WHEN: The get_url_file_size() method is called
size = get_url_file_size(fake_url) file_size = get_url_file_size(fake_url)
# THEN: The correct methods are called with the correct arguments and a web page is returned # THEN: The correct methods are called with the correct arguments and a web page is returned
mock_urlopen.assert_called_with(fake_url, timeout=30) mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, timeout=30.0)
assert file_size == 100
@patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') @patch('openlp.core.common.httputils.requests')
def test_socket_timeout(self, mocked_urlopen): def test_socket_timeout(self, mocked_requests):
""" """
Test socket timeout gets caught Test socket timeout gets caught
""" """
# GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download
mocked_urlopen.side_effect = socket.timeout() mocked_requests.get.side_effect = IOError
# WHEN: Attempt to retrieve a file # WHEN: Attempt to retrieve a file
url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile) url_get_file(MagicMock(), url='http://localhost/test', file_path=Path(self.tempfile))
# THEN: socket.timeout should have been caught # THEN: socket.timeout should have been caught
# NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files
self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') assert not os.path.exists(self.tempfile), 'tempfile should have been deleted'
def test_ping_valid(self):
"""
Test ping for OpenLP
"""
# GIVEN: a valid url to test
url = "openlp.io"
# WHEN: Attempt to check the url exists
url_found = ping(url)
# THEN: It should be found
self.assertTrue(url_found, 'OpenLP.io is not found')
def test_ping_invalid(self):
"""
Test ping for OpenLP
"""
# GIVEN: a valid url to test
url = "trb143.io"
# WHEN: Attempt to check the url exists
url_found = ping(url)
# THEN: It should be found
self.assertFalse(url_found, 'TRB143.io is found')

View File

@ -24,8 +24,209 @@ Package to test the openlp.core.common.path package.
""" """
import os import os
from unittest import TestCase from unittest import TestCase
from unittest.mock import ANY, MagicMock, patch
from openlp.core.common.path import Path, path_to_str, str_to_path from openlp.core.common.path import Path, copy, copyfile, copytree, path_to_str, replace_params, rmtree, str_to_path, \
which
class TestShutil(TestCase):
"""
Tests for the :mod:`openlp.core.common.path` module
"""
def test_replace_params_no_params(self):
"""
Test replace_params when called with and empty tuple instead of parameters to replace
"""
# GIVEN: Some test data
test_args = (1, 2)
test_kwargs = {'arg3': 3, 'arg4': 4}
test_params = tuple()
# WHEN: Calling replace_params
result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
# THEN: The positional and keyword args should not have changed
self.assertEqual(test_args, result_args)
self.assertEqual(test_kwargs, result_kwargs)
def test_replace_params_params(self):
"""
Test replace_params when given a positional and a keyword argument to change
"""
# GIVEN: Some test data
test_args = (1, 2)
test_kwargs = {'arg3': 3, 'arg4': 4}
test_params = ((1, 'arg2', str), (2, 'arg3', str))
# WHEN: Calling replace_params
result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
# THEN: The positional and keyword args should have have changed
self.assertEqual(result_args, (1, '2'))
self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4})
def test_copy(self):
"""
Test :func:`openlp.core.common.path.copy`
"""
# GIVEN: A mocked `shutil.copy` which returns a test path as a string
with patch('openlp.core.common.path.shutil.copy', return_value=os.path.join('destination', 'test', 'path')) \
as mocked_shutil_copy:
# WHEN: Calling :func:`openlp.core.common.path.copy` with the src and dst parameters as Path object types
result = copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
# THEN: :func:`shutil.copy` should have been called with the str equivalents of the Path objects.
# :func:`openlp.core.common.path.copy` should return the str type result of calling
# :func:`shutil.copy` as a Path object.
mocked_shutil_copy.assert_called_once_with(os.path.join('source', 'test', 'path'),
os.path.join('destination', 'test', 'path'))
self.assertEqual(result, Path('destination', 'test', 'path'))
def test_copy_follow_optional_params(self):
"""
Test :func:`openlp.core.common.path.copy` when follow_symlinks is set to false
"""
# GIVEN: A mocked `shutil.copy`
with patch('openlp.core.common.path.shutil.copy', return_value='') as mocked_shutil_copy:
# WHEN: Calling :func:`openlp.core.common.path.copy` with :param:`follow_symlinks` set to False
copy(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
# THEN: :func:`shutil.copy` should have been called with :param:`follow_symlinks` set to false
mocked_shutil_copy.assert_called_once_with(ANY, ANY, follow_symlinks=False)
def test_copyfile(self):
"""
Test :func:`openlp.core.common.path.copyfile`
"""
# GIVEN: A mocked :func:`shutil.copyfile` which returns a test path as a string
with patch('openlp.core.common.path.shutil.copyfile',
return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copyfile:
# WHEN: Calling :func:`openlp.core.common.path.copyfile` with the src and dst parameters as Path object
# types
result = copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
# THEN: :func:`shutil.copyfile` should have been called with the str equivalents of the Path objects.
# :func:`openlp.core.common.path.copyfile` should return the str type result of calling
# :func:`shutil.copyfile` as a Path object.
mocked_shutil_copyfile.assert_called_once_with(os.path.join('source', 'test', 'path'),
os.path.join('destination', 'test', 'path'))
self.assertEqual(result, Path('destination', 'test', 'path'))
def test_copyfile_optional_params(self):
"""
Test :func:`openlp.core.common.path.copyfile` when follow_symlinks is set to false
"""
# GIVEN: A mocked :func:`shutil.copyfile`
with patch('openlp.core.common.path.shutil.copyfile', return_value='') as mocked_shutil_copyfile:
# WHEN: Calling :func:`openlp.core.common.path.copyfile` with :param:`follow_symlinks` set to False
copyfile(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), follow_symlinks=False)
# THEN: :func:`shutil.copyfile` should have been called with the optional parameters, with out any of the
# values being modified
mocked_shutil_copyfile.assert_called_once_with(ANY, ANY, follow_symlinks=False)
def test_copytree(self):
"""
Test :func:`openlp.core.common.path.copytree`
"""
# GIVEN: A mocked :func:`shutil.copytree` which returns a test path as a string
with patch('openlp.core.common.path.shutil.copytree',
return_value=os.path.join('destination', 'test', 'path')) as mocked_shutil_copytree:
# WHEN: Calling :func:`openlp.core.common.path.copytree` with the src and dst parameters as Path object
# types
result = copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'))
# THEN: :func:`shutil.copytree` should have been called with the str equivalents of the Path objects.
# :func:`openlp.core.common.path.copytree` should return the str type result of calling
# :func:`shutil.copytree` as a Path object.
mocked_shutil_copytree.assert_called_once_with(os.path.join('source', 'test', 'path'),
os.path.join('destination', 'test', 'path'))
self.assertEqual(result, Path('destination', 'test', 'path'))
def test_copytree_optional_params(self):
"""
Test :func:`openlp.core.common.path.copytree` when optional parameters are passed
"""
# GIVEN: A mocked :func:`shutil.copytree`
with patch('openlp.core.common.path.shutil.copytree', return_value='') as mocked_shutil_copytree:
mocked_ignore = MagicMock()
mocked_copy_function = MagicMock()
# WHEN: Calling :func:`openlp.core.common.path.copytree` with the optional parameters set
copytree(Path('source', 'test', 'path'), Path('destination', 'test', 'path'), symlinks=True,
ignore=mocked_ignore, copy_function=mocked_copy_function, ignore_dangling_symlinks=True)
# THEN: :func:`shutil.copytree` should have been called with the optional parameters, with out any of the
# values being modified
mocked_shutil_copytree.assert_called_once_with(ANY, ANY, symlinks=True, ignore=mocked_ignore,
copy_function=mocked_copy_function,
ignore_dangling_symlinks=True)
def test_rmtree(self):
"""
Test :func:`rmtree`
"""
# GIVEN: A mocked :func:`shutil.rmtree`
with patch('openlp.core.common.path.shutil.rmtree', return_value=None) as mocked_shutil_rmtree:
# WHEN: Calling :func:`openlp.core.common.path.rmtree` with the path parameter as Path object type
result = rmtree(Path('test', 'path'))
# 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'))
self.assertIsNone(result)
def test_rmtree_optional_params(self):
"""
Test :func:`openlp.core.common.path.rmtree` when optional parameters are passed
"""
# GIVEN: A mocked :func:`shutil.rmtree`
with patch('openlp.core.common.path.shutil.rmtree', return_value='') as mocked_shutil_rmtree:
mocked_on_error = MagicMock()
# WHEN: Calling :func:`openlp.core.common.path.rmtree` with :param:`ignore_errors` set to True and
# :param:`onerror` set to a mocked object
rmtree(Path('test', 'path'), ignore_errors=True, onerror=mocked_on_error)
# 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(ANY, ignore_errors=True, onerror=mocked_on_error)
def test_which_no_command(self):
"""
Test :func:`openlp.core.common.path.which` when the command is not found.
"""
# GIVEN: A mocked :func:`shutil.which` when the command is not found.
with patch('openlp.core.common.path.shutil.which', return_value=None) as mocked_shutil_which:
# WHEN: Calling :func:`openlp.core.common.path.which` with a command that does not exist.
result = which('no_command')
# THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return None.
mocked_shutil_which.assert_called_once_with('no_command')
self.assertIsNone(result)
def test_which_command(self):
"""
Test :func:`openlp.core.common.path.which` when a command has been found.
"""
# GIVEN: A mocked :func:`shutil.which` when the command is found.
with patch('openlp.core.common.path.shutil.which',
return_value=os.path.join('path', 'to', 'command')) as mocked_shutil_which:
# WHEN: Calling :func:`openlp.core.common.path.which` with a command that exists.
result = which('command')
# THEN: :func:`shutil.which` should have been called with the command, and :func:`which` should return a
# Path object equivalent of the command path.
mocked_shutil_which.assert_called_once_with('command')
self.assertEqual(result, Path('path', 'to', 'command'))
class TestPath(TestCase): class TestPath(TestCase):

View File

@ -32,7 +32,7 @@ from PyQt5 import QtCore, QtGui
from openlp.core.common.path import Path from openlp.core.common.path import Path
from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \ from openlp.core.lib import FormattingTags, build_icon, check_item_selected, clean_tags, compare_chord_lyric, \
create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \ create_separated_list, create_thumb, expand_chords, expand_chords_for_printing, expand_tags, find_formatting_tags, \
get_text_file_string, image_to_byte, replace_params, resize_image, str_to_bool, validate_thumb get_text_file_string, image_to_byte, resize_image, str_to_bool, validate_thumb
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources')) TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
@ -595,93 +595,46 @@ class TestLib(TestCase):
Test the validate_thumb() function when the thumbnail does not exist Test the validate_thumb() function when the thumbnail does not exist
""" """
# GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb
with patch('openlp.core.lib.os') as mocked_os: with patch.object(Path, 'exists', return_value=False) as mocked_path_exists:
file_path = 'path/to/file' file_path = Path('path', 'to', 'file')
thumb_path = 'path/to/thumb' thumb_path = Path('path', 'to', 'thumb')
mocked_os.path.exists.return_value = False
# WHEN: we run the validate_thumb() function # WHEN: we run the validate_thumb() function
result = validate_thumb(file_path, thumb_path) result = validate_thumb(file_path, thumb_path)
# THEN: we should have called a few functions, and the result should be False # THEN: we should have called a few functions, and the result should be False
mocked_os.path.exists.assert_called_with(thumb_path) thumb_path.exists.assert_called_once_with()
assert result is False, 'The result should be False' self.assertFalse(result, 'The result should be False')
def test_validate_thumb_file_exists_and_newer(self): def test_validate_thumb_file_exists_and_newer(self):
""" """
Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file
""" """
# GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb with patch.object(Path, 'exists'), patch.object(Path, 'stat'):
with patch('openlp.core.lib.os') as mocked_os: # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
file_path = 'path/to/file' file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
thumb_path = 'path/to/thumb' thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)})
file_mocked_stat = MagicMock()
file_mocked_stat.st_mtime = datetime.now()
thumb_mocked_stat = MagicMock()
thumb_mocked_stat.st_mtime = datetime.now() + timedelta(seconds=10)
mocked_os.path.exists.return_value = True
mocked_os.stat.side_effect = [file_mocked_stat, thumb_mocked_stat]
# WHEN: we run the validate_thumb() function # WHEN: we run the validate_thumb() function
result = validate_thumb(file_path, thumb_path)
# THEN: we should have called a few functions, and the result should be True # THEN: `validate_thumb` should return True
# mocked_os.path.exists.assert_called_with(thumb_path) self.assertTrue(result)
def test_validate_thumb_file_exists_and_older(self): def test_validate_thumb_file_exists_and_older(self):
""" """
Test the validate_thumb() function when the thumbnail exists but is older than the file Test the validate_thumb() function when the thumbnail exists but is older than the file
""" """
# GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
with patch('openlp.core.lib.os') as mocked_os: file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
file_path = 'path/to/file' thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)})
thumb_path = 'path/to/thumb'
file_mocked_stat = MagicMock()
file_mocked_stat.st_mtime = datetime.now()
thumb_mocked_stat = MagicMock()
thumb_mocked_stat.st_mtime = datetime.now() - timedelta(seconds=10)
mocked_os.path.exists.return_value = True
mocked_os.stat.side_effect = lambda fname: file_mocked_stat if fname == file_path else thumb_mocked_stat
# WHEN: we run the validate_thumb() function # WHEN: we run the validate_thumb() function
result = validate_thumb(file_path, thumb_path) result = validate_thumb(file_path, thumb_path)
# THEN: we should have called a few functions, and the result should be False # THEN: `validate_thumb` should return False
mocked_os.path.exists.assert_called_with(thumb_path) thumb_path.stat.assert_called_once_with()
mocked_os.stat.assert_any_call(file_path) self.assertFalse(result, 'The result should be False')
mocked_os.stat.assert_any_call(thumb_path)
assert result is False, 'The result should be False'
def test_replace_params_no_params(self):
"""
Test replace_params when called with and empty tuple instead of parameters to replace
"""
# GIVEN: Some test data
test_args = (1, 2)
test_kwargs = {'arg3': 3, 'arg4': 4}
test_params = tuple()
# WHEN: Calling replace_params
result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
# THEN: The positional and keyword args should not have changed
self.assertEqual(test_args, result_args)
self.assertEqual(test_kwargs, result_kwargs)
def test_replace_params_params(self):
"""
Test replace_params when given a positional and a keyword argument to change
"""
# GIVEN: Some test data
test_args = (1, 2)
test_kwargs = {'arg3': 3, 'arg4': 4}
test_params = ((1, 'arg2', str), (2, 'arg3', str))
# WHEN: Calling replace_params
result_args, result_kwargs = replace_params(test_args, test_kwargs, test_params)
# THEN: The positional and keyword args should have have changed
self.assertEqual(result_args, (1, '2'))
self.assertEqual(result_kwargs, {'arg3': '3', 'arg4': 4})
def test_resize_thumb(self): def test_resize_thumb(self):
""" """

View File

@ -111,7 +111,7 @@ class TestProjectorDBUpdate(TestCase):
""" """
Test that we can upgrade an old song db to the current schema Test that we can upgrade an old song db to the current schema
""" """
# GIVEN: An old song db # GIVEN: An old prjector db
old_db = os.path.join(TEST_RESOURCES_PATH, "projector", TEST_DB_PJLINK1) old_db = os.path.join(TEST_RESOURCES_PATH, "projector", TEST_DB_PJLINK1)
tmp_db = os.path.join(self.tmp_folder, TEST_DB) tmp_db = os.path.join(self.tmp_folder, TEST_DB)
shutil.copyfile(old_db, tmp_db) shutil.copyfile(old_db, tmp_db)

View File

@ -25,12 +25,13 @@ Package to test the openlp.core.lib.projector.pjlink base package.
from unittest import TestCase from unittest import TestCase
from unittest.mock import call, patch, MagicMock from unittest.mock import call, patch, MagicMock
from openlp.core.lib.projector.db import Projector
from openlp.core.lib.projector.pjlink import PJLink from openlp.core.lib.projector.pjlink import PJLink
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_ON, S_CONNECTED
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH, TEST1_DATA
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
class TestPJLinkBase(TestCase): class TestPJLinkBase(TestCase):

View File

@ -27,6 +27,7 @@ from unittest import TestCase
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import openlp.core.lib.projector.pjlink import openlp.core.lib.projector.pjlink
from openlp.core.lib.projector.db import Projector
from openlp.core.lib.projector.pjlink import PJLink from openlp.core.lib.projector.pjlink import PJLink
from openlp.core.lib.projector.constants import PJLINK_ERRORS, \ from openlp.core.lib.projector.constants import PJLINK_ERRORS, \
E_AUTHENTICATION, E_PARAMETER, E_PROJECTOR, E_UNAVAILABLE, E_UNDEFINED E_AUTHENTICATION, E_PARAMETER, E_PROJECTOR, E_UNAVAILABLE, E_UNDEFINED
@ -35,9 +36,10 @@ from openlp.core.lib.projector.constants import PJLINK_ERRORS, \
from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \
PJLINK_POWR_STATUS, PJLINK_VALID_CMD, E_WARN, E_ERROR, S_OFF, S_STANDBY, S_ON PJLINK_POWR_STATUS, PJLINK_VALID_CMD, E_WARN, E_ERROR, S_OFF, S_STANDBY, S_ON
''' '''
from tests.resources.projector.data import TEST_PIN from tests.resources.projector.data import TEST_PIN, TEST1_DATA
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) pjlink_test = PJLink(Projector(**TEST1_DATA), pin=TEST_PIN, no_poll=True)
pjlink_test.ip = '127.0.0.1'
class TestPJLinkRouting(TestCase): class TestPJLinkRouting(TestCase):

View File

@ -26,15 +26,17 @@ from unittest import TestCase
from unittest.mock import patch from unittest.mock import patch
import openlp.core.lib.projector.pjlink import openlp.core.lib.projector.pjlink
from openlp.core.lib.projector.db import Projector
from openlp.core.lib.projector.pjlink import PJLink from openlp.core.lib.projector.pjlink import PJLink
from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \ from openlp.core.lib.projector.constants import ERROR_STRING, PJLINK_ERST_DATA, PJLINK_ERST_STATUS, \
PJLINK_POWR_STATUS, \ PJLINK_POWR_STATUS, \
E_ERROR, E_NOT_CONNECTED, E_SOCKET_ADDRESS_NOT_AVAILABLE, E_UNKNOWN_SOCKET_ERROR, E_WARN, \ E_ERROR, E_NOT_CONNECTED, E_SOCKET_ADDRESS_NOT_AVAILABLE, E_UNKNOWN_SOCKET_ERROR, E_WARN, \
S_CONNECTED, S_OFF, S_ON, S_NOT_CONNECTED, S_CONNECTING, S_STANDBY S_CONNECTED, S_OFF, S_ON, S_NOT_CONNECTED, S_CONNECTING, S_STANDBY
from tests.resources.projector.data import TEST_PIN from tests.resources.projector.data import TEST_PIN, TEST1_DATA
pjlink_test = PJLink(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) pjlink_test = PJLink(Projector(**TEST1_DATA), pin=TEST_PIN, no_poll=True)
pjlink_test.ip = '127.0.0.1'
# Create a list of ERST positional data so we don't have to redo the same buildup multiple times # Create a list of ERST positional data so we don't have to redo the same buildup multiple times
PJLINK_ERST_POSITIONS = [] PJLINK_ERST_POSITIONS = []

View File

@ -22,8 +22,9 @@
""" """
Package to test the openlp.core.lib.theme package. Package to test the openlp.core.lib.theme package.
""" """
from unittest import TestCase
import os import os
from pathlib import Path
from unittest import TestCase
from openlp.core.lib.theme import Theme from openlp.core.lib.theme import Theme
@ -79,16 +80,16 @@ class TestTheme(TestCase):
""" """
# GIVEN: A theme object # GIVEN: A theme object
theme = Theme() theme = Theme()
theme.theme_name = 'MyBeautifulTheme ' theme.theme_name = 'MyBeautifulTheme'
theme.background_filename = ' video.mp4' theme.background_filename = Path('video.mp4')
theme.background_type = 'video' theme.background_type = 'video'
path = os.path.expanduser('~') path = Path.home()
# WHEN: Theme.extend_image_filename is run # WHEN: Theme.extend_image_filename is run
theme.extend_image_filename(path) theme.extend_image_filename(path)
# THEN: The filename of the background should be correct # THEN: The filename of the background should be correct
expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4') expected_filename = path / 'MyBeautifulTheme' / 'video.mp4'
self.assertEqual(expected_filename, theme.background_filename) self.assertEqual(expected_filename, theme.background_filename)
self.assertEqual('MyBeautifulTheme', theme.theme_name) self.assertEqual('MyBeautifulTheme', theme.theme_name)

View File

@ -47,13 +47,13 @@ class TestFirstTimeForm(TestCase, TestMixin):
# THEN: A web browser is opened # THEN: A web browser is opened
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute') mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute')
@patch('openlp.core.ui.aboutform.get_application_version') @patch('openlp.core.ui.aboutform.get_version')
def test_about_form_build_number(self, mocked_get_application_version): def test_about_form_build_number(self, mocked_get_version):
""" """
Test that the build number is added to the about form Test that the build number is added to the about form
""" """
# GIVEN: A mocked out get_application_version function # GIVEN: A mocked out get_version function
mocked_get_application_version.return_value = {'version': '3.1.5', 'build': '3000'} mocked_get_version.return_value = {'version': '3.1.5', 'build': '3000'}
# WHEN: The about form is created # WHEN: The about form is created
about_form = AboutForm(None) about_form = AboutForm(None)

View File

@ -22,11 +22,11 @@
""" """
Package to test the openlp.core.ui.exeptionform package. Package to test the openlp.core.ui.exeptionform package.
""" """
import os import os
import tempfile import tempfile
from unittest import TestCase from unittest import TestCase
from unittest.mock import mock_open, patch from unittest.mock import call, patch
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.core.common.path import Path from openlp.core.common.path import Path
@ -53,7 +53,7 @@ MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of
@patch("openlp.core.ui.exceptionform.Qt.qVersion") @patch("openlp.core.ui.exceptionform.Qt.qVersion")
@patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl") @patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl")
@patch("openlp.core.ui.exceptionform.get_application_version") @patch("openlp.core.ui.exceptionform.get_version")
@patch("openlp.core.ui.exceptionform.sqlalchemy") @patch("openlp.core.ui.exceptionform.sqlalchemy")
@patch("openlp.core.ui.exceptionform.bs4") @patch("openlp.core.ui.exceptionform.bs4")
@patch("openlp.core.ui.exceptionform.etree") @patch("openlp.core.ui.exceptionform.etree")
@ -64,18 +64,10 @@ class TestExceptionForm(TestMixin, TestCase):
""" """
Test functionality of exception form functions Test functionality of exception form functions
""" """
def __method_template_for_class_patches(self, def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
__PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__, mocked_python_version, mocked_platform, mocked_is_linux,
mocked_python_version, mocked_etree, mocked_bs4, mocked_sqlalchemy, mocked_get_version,
mocked_platform, mocked_openlurl, mocked_qversion):
mocked_is_linux,
mocked_etree,
mocked_bs4,
mocked_sqlalchemy,
mocked_application_version,
mocked_openlurl,
mocked_qversion,
):
""" """
Template so you don't have to remember the layout of class mock options for methods Template so you don't have to remember the layout of class mock options for methods
""" """
@ -86,7 +78,7 @@ class TestExceptionForm(TestMixin, TestCase):
mocked_platform.return_value = 'Nose Test' mocked_platform.return_value = 'Nose Test'
mocked_qversion.return_value = 'Qt5 test' mocked_qversion.return_value = 'Qt5 test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_application_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
def setUp(self): def setUp(self):
self.setup_application() self.setup_application()
@ -103,26 +95,14 @@ class TestExceptionForm(TestMixin, TestCase):
os.remove(self.tempfile) os.remove(self.tempfile)
@patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog") @patch("openlp.core.ui.exceptionform.Ui_ExceptionDialog")
@patch("openlp.core.ui.exceptionform.QtWidgets.QFileDialog") @patch("openlp.core.ui.exceptionform.FileDialog")
@patch("openlp.core.ui.exceptionform.QtCore.QUrl") @patch("openlp.core.ui.exceptionform.QtCore.QUrl")
@patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem") @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem")
@patch("openlp.core.ui.exceptionform.Qt") @patch("openlp.core.ui.exceptionform.Qt")
def test_on_send_report_button_clicked(self, def test_on_send_report_button_clicked(self, mocked_qt, mocked_add_query_item, mocked_qurl, mocked_file_dialog,
mocked_qt, mocked_ui_exception_dialog, mocked_python_version, mocked_platform,
mocked_add_query_item, mocked_is_linux, mocked_etree, mocked_bs4, mocked_sqlalchemy,
mocked_qurl, mocked_get_version, mocked_openlurl, mocked_qversion):
mocked_file_dialog,
mocked_ui_exception_dialog,
mocked_python_version,
mocked_platform,
mocked_is_linux,
mocked_etree,
mocked_bs4,
mocked_sqlalchemy,
mocked_application_version,
mocked_openlurl,
mocked_qversion,
):
""" """
Test send report creates the proper system information text Test send report creates the proper system information text
""" """
@ -134,42 +114,33 @@ class TestExceptionForm(TestMixin, TestCase):
mocked_platform.return_value = 'Nose Test' mocked_platform.return_value = 'Nose Test'
mocked_qversion.return_value = 'Qt5 test' mocked_qversion.return_value = 'Qt5 test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_application_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_application_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
test_form = exceptionform.ExceptionForm() test_form = exceptionform.ExceptionForm()
test_form.file_attachment = None test_form.file_attachment = None
with patch.object(test_form, '_pyuno_import') as mock_pyuno: with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test' mock_pyuno.return_value = 'UNO Bridge Test'
mock_traceback.return_value = 'openlp: Traceback Test' mock_traceback.return_value = 'openlp: Traceback Test'
mock_description.return_value = 'Description Test' mock_description.return_value = 'Description Test'
# WHEN: on_save_report_button_clicked called # WHEN: on_save_report_button_clicked called
test_form.on_send_report_button_clicked() test_form.on_send_report_button_clicked()
# THEN: Verify strings were formatted properly # THEN: Verify strings were formatted properly
mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT) mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
@patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName") @patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName")
@patch("openlp.core.ui.exceptionform.Qt") @patch("openlp.core.ui.exceptionform.Qt")
def test_on_save_report_button_clicked(self, def test_on_save_report_button_clicked(self, mocked_qt, mocked_save_filename, mocked_python_version,
mocked_qt, mocked_platform, mocked_is_linux, mocked_etree, mocked_bs4,
mocked_save_filename, mocked_sqlalchemy, mocked_get_version, mocked_openlurl,
mocked_python_version, mocked_qversion):
mocked_platform,
mocked_is_linux,
mocked_etree,
mocked_bs4,
mocked_sqlalchemy,
mocked_application_version,
mocked_openlurl,
mocked_qversion,
):
""" """
Test save report saves the correct information to a file Test save report saves the correct information to a file
""" """
@ -181,26 +152,25 @@ class TestExceptionForm(TestMixin, TestCase):
mocked_qversion.return_value = 'Qt5 test' mocked_qversion.return_value = 'Qt5 test'
mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_application_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
mocked_save_filename.return_value = (Path('testfile.txt'), 'filter')
test_form = exceptionform.ExceptionForm() with patch.object(Path, 'open') as mocked_path_open:
test_form.file_attachment = None test_path = Path('testfile.txt')
mocked_save_filename.return_value = test_path, 'ext'
with patch.object(test_form, '_pyuno_import') as mock_pyuno: test_form = exceptionform.ExceptionForm()
with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback: test_form.file_attachment = None
with patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
with patch("openlp.core.ui.exceptionform.open", mock_open(), create=True) as mocked_open:
mock_pyuno.return_value = 'UNO Bridge Test'
mock_traceback.return_value = 'openlp: Traceback Test'
mock_description.return_value = 'Description Test'
# WHEN: on_save_report_button_clicked called with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
test_form.on_save_report_button_clicked() patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test'
mock_traceback.return_value = 'openlp: Traceback Test'
mock_description.return_value = 'Description Test'
# WHEN: on_save_report_button_clicked called
test_form.on_save_report_button_clicked()
# THEN: Verify proper calls to save file # THEN: Verify proper calls to save file
# self.maxDiff = None # self.maxDiff = None
check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__()) mocked_path_open.assert_has_calls([call().__enter__().write(MAIL_ITEM_TEXT)])
write_text = "{text}".format(text=mocked_open.mock_calls[1])
mocked_open.assert_called_with('testfile.txt', 'w')
self.assertEquals(check_text, write_text, "Saved information should match test text")

View File

@ -22,9 +22,6 @@
""" """
Package to test the openlp.core.utils.__init__ package. Package to test the openlp.core.utils.__init__ package.
""" """
import urllib.request
import urllib.error
import urllib.parse
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch from unittest.mock import patch
@ -37,20 +34,21 @@ class TestFirstTimeWizard(TestMixin, TestCase):
""" """
Test First Time Wizard import functions Test First Time Wizard import functions
""" """
def test_webpage_connection_retry(self): @patch('openlp.core.common.httputils.requests')
def test_webpage_connection_retry(self, mocked_requests):
""" """
Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031 Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031
""" """
# GIVEN: Initial settings and mocks # GIVEN: Initial settings and mocks
with patch.object(urllib.request, 'urlopen') as mocked_urlopen: mocked_requests.get.side_effect = IOError('Unable to connect')
mocked_urlopen.side_effect = ConnectionError
# WHEN: A webpage is requested # WHEN: A webpage is requested
try: try:
get_web_page(url='http://localhost') get_web_page('http://localhost')
except: except Exception as e:
pass assert isinstance(e, ConnectionError)
# THEN: urlopen should have been called CONNECTION_RETRIES + 1 count # THEN: urlopen should have been called CONNECTION_RETRIES + 1 count
self.assertEquals(mocked_urlopen.call_count, CONNECTION_RETRIES + 1, assert mocked_requests.get.call_count == CONNECTION_RETRIES, \
'get_web_page() should have tried {} times'.format(CONNECTION_RETRIES)) 'get should have been called {} times, but was only called {} times'.format(
CONNECTION_RETRIES, mocked_requests.get.call_count)

View File

@ -34,7 +34,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
FAKE_CONFIG = b""" FAKE_CONFIG = """
[general] [general]
base url = http://example.com/frw/ base url = http://example.com/frw/
[songs] [songs]
@ -45,7 +45,7 @@ directory = bibles
directory = themes directory = themes
""" """
FAKE_BROKEN_CONFIG = b""" FAKE_BROKEN_CONFIG = """
[general] [general]
base url = http://example.com/frw/ base url = http://example.com/frw/
[songs] [songs]
@ -54,7 +54,7 @@ directory = songs
directory = bibles directory = bibles
""" """
FAKE_INVALID_CONFIG = b""" FAKE_INVALID_CONFIG = """
<html> <html>
<head><title>This is not a config file</title></head> <head><title>This is not a config file</title></head>
<body>Some text</body> <body>Some text</body>
@ -112,7 +112,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
patch('openlp.core.ui.firsttimeform.Settings') as MockedSettings, \ patch('openlp.core.ui.firsttimeform.Settings') as MockedSettings, \
patch('openlp.core.ui.firsttimeform.gettempdir') as mocked_gettempdir, \ patch('openlp.core.ui.firsttimeform.gettempdir') as mocked_gettempdir, \
patch('openlp.core.ui.firsttimeform.check_directory_exists') as mocked_check_directory_exists, \ patch('openlp.core.ui.firsttimeform.check_directory_exists') as mocked_check_directory_exists, \
patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: patch.object(frw.application, 'set_normal_cursor'):
mocked_settings = MagicMock() mocked_settings = MagicMock()
mocked_settings.value.return_value = True mocked_settings.value.return_value = True
MockedSettings.return_value = mocked_settings MockedSettings.return_value = mocked_settings
@ -192,7 +192,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:
first_time_form = FirstTimeForm(None) first_time_form = FirstTimeForm(None)
first_time_form.initialize(MagicMock()) first_time_form.initialize(MagicMock())
mocked_get_web_page.return_value.read.return_value = FAKE_BROKEN_CONFIG mocked_get_web_page.return_value = FAKE_BROKEN_CONFIG
# WHEN: The First Time Wizard is downloads the config file # WHEN: The First Time Wizard is downloads the config file
first_time_form._download_index() first_time_form._download_index()
@ -208,7 +208,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page: with patch('openlp.core.ui.firsttimeform.get_web_page') as mocked_get_web_page:
first_time_form = FirstTimeForm(None) first_time_form = FirstTimeForm(None)
first_time_form.initialize(MagicMock()) first_time_form.initialize(MagicMock())
mocked_get_web_page.return_value.read.return_value = FAKE_INVALID_CONFIG mocked_get_web_page.return_value = FAKE_INVALID_CONFIG
# WHEN: The First Time Wizard is downloads the config file # WHEN: The First Time Wizard is downloads the config file
first_time_form._download_index() first_time_form._download_index()
@ -225,14 +225,13 @@ class TestFirstTimeForm(TestCase, TestMixin):
# GIVEN: Initial setup and mocks # GIVEN: Initial setup and mocks
first_time_form = FirstTimeForm(None) first_time_form = FirstTimeForm(None)
first_time_form.initialize(MagicMock()) first_time_form.initialize(MagicMock())
mocked_get_web_page.side_effect = urllib.error.HTTPError(url='http//localhost', mocked_get_web_page.side_effect = ConnectionError('')
code=407, mocked_message_box.Ok = 'OK'
msg='Network proxy error',
hdrs=None,
fp=None)
# WHEN: the First Time Wizard calls to get the initial configuration # WHEN: the First Time Wizard calls to get the initial configuration
first_time_form._download_index() first_time_form._download_index()
# THEN: the critical_error_message_box should have been called # THEN: the critical_error_message_box should have been called
self.assertEquals(mocked_message_box.mock_calls[1][1][0], 'Network Error 407', mocked_message_box.critical.assert_called_once_with(
'first_time_form should have caught Network Error') first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve '
'initial configuration information', 'OK')

View File

@ -27,10 +27,10 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import Registry, is_macosx, Settings from openlp.core.common import Registry, is_macosx
from openlp.core.common.path import Path
from openlp.core.lib import ScreenList, PluginManager from openlp.core.lib import ScreenList, PluginManager
from openlp.core.ui import MainDisplay, AudioPlayer from openlp.core.ui import MainDisplay, AudioPlayer
from openlp.core.ui.media import MediaController
from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -184,7 +184,7 @@ class TestMainDisplay(TestCase, TestMixin):
self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged, self.assertEqual(pyobjc_nsview.window().collectionBehavior(), NSWindowCollectionBehaviorManaged,
'Window collection behavior should be NSWindowCollectionBehaviorManaged') 'Window collection behavior should be NSWindowCollectionBehaviorManaged')
@patch(u'openlp.core.ui.maindisplay.Settings') @patch('openlp.core.ui.maindisplay.Settings')
def test_show_display_startup_logo(self, MockedSettings): def test_show_display_startup_logo(self, MockedSettings):
# GIVEN: Mocked show_display, setting for logo visibility # GIVEN: Mocked show_display, setting for logo visibility
display = MagicMock() display = MagicMock()
@ -204,7 +204,7 @@ class TestMainDisplay(TestCase, TestMixin):
# THEN: setVisible should had been called with "True" # THEN: setVisible should had been called with "True"
main_display.setVisible.assert_called_once_with(True) main_display.setVisible.assert_called_once_with(True)
@patch(u'openlp.core.ui.maindisplay.Settings') @patch('openlp.core.ui.maindisplay.Settings')
def test_show_display_hide_startup_logo(self, MockedSettings): def test_show_display_hide_startup_logo(self, MockedSettings):
# GIVEN: Mocked show_display, setting for logo visibility # GIVEN: Mocked show_display, setting for logo visibility
display = MagicMock() display = MagicMock()
@ -224,8 +224,8 @@ class TestMainDisplay(TestCase, TestMixin):
# THEN: setVisible should had not been called # THEN: setVisible should had not been called
main_display.setVisible.assert_not_called() main_display.setVisible.assert_not_called()
@patch(u'openlp.core.ui.maindisplay.Settings') @patch('openlp.core.ui.maindisplay.Settings')
@patch(u'openlp.core.ui.maindisplay.build_html') @patch('openlp.core.ui.maindisplay.build_html')
def test_build_html_no_video(self, MockedSettings, Mocked_build_html): def test_build_html_no_video(self, MockedSettings, Mocked_build_html):
# GIVEN: Mocked display # GIVEN: Mocked display
display = MagicMock() display = MagicMock()
@ -252,8 +252,8 @@ class TestMainDisplay(TestCase, TestMixin):
self.assertEquals(main_display.media_controller.video.call_count, 0, self.assertEquals(main_display.media_controller.video.call_count, 0,
'Media Controller video should not have been called') 'Media Controller video should not have been called')
@patch(u'openlp.core.ui.maindisplay.Settings') @patch('openlp.core.ui.maindisplay.Settings')
@patch(u'openlp.core.ui.maindisplay.build_html') @patch('openlp.core.ui.maindisplay.build_html')
def test_build_html_video(self, MockedSettings, Mocked_build_html): def test_build_html_video(self, MockedSettings, Mocked_build_html):
# GIVEN: Mocked display # GIVEN: Mocked display
display = MagicMock() display = MagicMock()
@ -270,7 +270,7 @@ class TestMainDisplay(TestCase, TestMixin):
service_item.theme_data = MagicMock() service_item.theme_data = MagicMock()
service_item.theme_data.background_type = 'video' service_item.theme_data.background_type = 'video'
service_item.theme_data.theme_name = 'name' service_item.theme_data.theme_name = 'name'
service_item.theme_data.background_filename = 'background_filename' service_item.theme_data.background_filename = Path('background_filename')
mocked_plugin = MagicMock() mocked_plugin = MagicMock()
display.plugin_manager = PluginManager() display.plugin_manager = PluginManager()
display.plugin_manager.plugins = [mocked_plugin] display.plugin_manager.plugins = [mocked_plugin]

View File

@ -49,5 +49,5 @@ class TestThemeManager(TestCase):
self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h')) self.instance.on_image_path_edit_path_changed(Path('/', 'new', 'pat.h'))
# THEN: The theme background file should be set and `set_background_page_values` should have been called # THEN: The theme background file should be set and `set_background_page_values` should have been called
self.assertEqual(self.instance.theme.background_filename, '/new/pat.h') self.assertEqual(self.instance.theme.background_filename, Path('/', 'new', 'pat.h'))
mocked_set_background_page_values.assert_called_once_with() mocked_set_background_page_values.assert_called_once_with()

View File

@ -30,8 +30,9 @@ from unittest.mock import ANY, MagicMock, patch
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from openlp.core.ui import ThemeManager
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.core.common.path import Path
from openlp.core.ui import ThemeManager
from tests.utils.constants import TEST_RESOURCES_PATH from tests.utils.constants import TEST_RESOURCES_PATH
@ -57,13 +58,13 @@ class TestThemeManager(TestCase):
""" """
# GIVEN: A new ThemeManager instance. # GIVEN: A new ThemeManager instance.
theme_manager = ThemeManager() theme_manager = ThemeManager()
theme_manager.path = os.path.join(TEST_RESOURCES_PATH, 'themes') theme_manager.theme_path = Path(TEST_RESOURCES_PATH, 'themes')
with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \ with patch('zipfile.ZipFile.__init__') as mocked_zipfile_init, \
patch('zipfile.ZipFile.write') as mocked_zipfile_write: patch('zipfile.ZipFile.write') as mocked_zipfile_write:
mocked_zipfile_init.return_value = None mocked_zipfile_init.return_value = None
# WHEN: The theme is exported # WHEN: The theme is exported
theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default') theme_manager._export_theme(Path('some', 'path', 'Default.otz'), 'Default')
# THEN: The zipfile should be created at the given path # 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_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
@ -86,57 +87,49 @@ class TestThemeManager(TestCase):
""" """
Test that we don't try to overwrite a theme background image with itself Test that we don't try to overwrite a theme background image with itself
""" """
# GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile, # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
# theme, check_directory_exists and thememanager-attributes. # theme, check_directory_exists and thememanager-attributes.
with patch('builtins.open') as mocked_open, \ with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
patch('openlp.core.ui.thememanager.check_directory_exists'): patch('openlp.core.ui.thememanager.check_directory_exists'):
mocked_open.return_value = MagicMock()
theme_manager = ThemeManager(None) theme_manager = ThemeManager(None)
theme_manager.old_background_image = None theme_manager.old_background_image = None
theme_manager.generate_and_save_image = MagicMock() theme_manager.generate_and_save_image = MagicMock()
theme_manager.path = '' theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock() mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename' mocked_theme.theme_name = 'themename'
mocked_theme.extract_formatted_xml = MagicMock() mocked_theme.extract_formatted_xml = MagicMock()
mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode() mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
# WHEN: Calling _write_theme with path to the same image, but the path written slightly different # WHEN: Calling _write_theme with path to the same image, but the path written slightly different
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg') file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
# Do replacement from end of string to avoid problems with path start theme_manager._write_theme(mocked_theme, file_name1, file_name1)
file_name2 = file_name1[::-1].replace(os.sep, os.sep + os.sep, 2)[::-1]
theme_manager._write_theme(mocked_theme, file_name1, file_name2)
# THEN: The mocked_copyfile should not have been called # THEN: The mocked_copyfile should not have been called
self.assertFalse(mocked_copyfile.called, 'shutil.copyfile should not be called') self.assertFalse(mocked_copyfile.called, 'copyfile should not be called')
def test_write_theme_diff_images(self): def test_write_theme_diff_images(self):
""" """
Test that we do overwrite a theme background image when a new is submitted Test that we do overwrite a theme background image when a new is submitted
""" """
# GIVEN: A new theme manager instance, with mocked builtins.open, shutil.copyfile, # GIVEN: A new theme manager instance, with mocked builtins.open, copyfile,
# theme, check_directory_exists and thememanager-attributes. # theme, check_directory_exists and thememanager-attributes.
with patch('builtins.open') as mocked_open, \ with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
patch('openlp.core.ui.thememanager.check_directory_exists'): patch('openlp.core.ui.thememanager.check_directory_exists'):
mocked_open.return_value = MagicMock()
theme_manager = ThemeManager(None) theme_manager = ThemeManager(None)
theme_manager.old_background_image = None theme_manager.old_background_image = None
theme_manager.generate_and_save_image = MagicMock() theme_manager.generate_and_save_image = MagicMock()
theme_manager.path = '' theme_manager.theme_path = MagicMock()
mocked_theme = MagicMock() mocked_theme = MagicMock()
mocked_theme.theme_name = 'themename' mocked_theme.theme_name = 'themename'
mocked_theme.filename = "filename" mocked_theme.filename = "filename"
# mocked_theme.extract_formatted_xml = MagicMock()
# mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
# WHEN: Calling _write_theme with path to different images # WHEN: Calling _write_theme with path to different images
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg') file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
file_name2 = os.path.join(TEST_RESOURCES_PATH, 'church2.jpg') file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg')
theme_manager._write_theme(mocked_theme, file_name1, file_name2) theme_manager._write_theme(mocked_theme, file_name1, file_name2)
# THEN: The mocked_copyfile should not have been called # THEN: The mocked_copyfile should not have been called
self.assertTrue(mocked_copyfile.called, 'shutil.copyfile should be called') self.assertTrue(mocked_copyfile.called, 'copyfile should be called')
def test_write_theme_special_char_name(self): def test_write_theme_special_char_name(self):
""" """
@ -146,7 +139,7 @@ class TestThemeManager(TestCase):
theme_manager = ThemeManager(None) theme_manager = ThemeManager(None)
theme_manager.old_background_image = None theme_manager.old_background_image = None
theme_manager.generate_and_save_image = MagicMock() theme_manager.generate_and_save_image = MagicMock()
theme_manager.path = self.temp_folder theme_manager.theme_path = Path(self.temp_folder)
mocked_theme = MagicMock() mocked_theme = MagicMock()
mocked_theme.theme_name = 'theme 愛 name' mocked_theme.theme_name = 'theme 愛 name'
mocked_theme.export_theme.return_value = "{}" mocked_theme.export_theme.return_value = "{}"
@ -208,17 +201,17 @@ class TestThemeManager(TestCase):
theme_manager = ThemeManager(None) theme_manager = ThemeManager(None)
theme_manager._create_theme_from_xml = MagicMock() theme_manager._create_theme_from_xml = MagicMock()
theme_manager.generate_and_save_image = MagicMock() theme_manager.generate_and_save_image = MagicMock()
theme_manager.path = '' theme_manager.theme_path = None
folder = mkdtemp() folder = Path(mkdtemp())
theme_file = os.path.join(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz') theme_file = Path(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
# WHEN: We try to unzip it # WHEN: We try to unzip it
theme_manager.unzip_theme(theme_file, folder) theme_manager.unzip_theme(theme_file, folder)
# THEN: Files should be unpacked # THEN: Files should be unpacked
self.assertTrue(os.path.exists(os.path.join(folder, 'Moss on tree', 'Moss on tree.xml'))) self.assertTrue((folder / 'Moss on tree' / 'Moss on tree.xml').exists())
self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened') self.assertEqual(mocked_critical_error_message_box.call_count, 0, 'No errors should have happened')
shutil.rmtree(folder) shutil.rmtree(str(folder))
def test_unzip_theme_invalid_version(self): def test_unzip_theme_invalid_version(self):
""" """

View File

@ -58,7 +58,7 @@ class TestImageMediaItem(TestCase):
Test that the validate_and_load_test() method when called without a group Test that the validate_and_load_test() method when called without a group
""" """
# GIVEN: A list of files # GIVEN: A list of files
file_list = ['/path1/image1.jpg', '/path2/image2.jpg'] file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
# WHEN: Calling validate_and_load with the list of files # WHEN: Calling validate_and_load with the list of files
self.media_item.validate_and_load(file_list) self.media_item.validate_and_load(file_list)
@ -66,7 +66,7 @@ class TestImageMediaItem(TestCase):
# THEN: load_list should have been called with the file list and None, # THEN: load_list should have been called with the file list and None,
# the directory should have been saved to the settings # the directory should have been saved to the settings
mocked_load_list.assert_called_once_with(file_list, None) mocked_load_list.assert_called_once_with(file_list, None)
mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) mocked_settings().setValue.assert_called_once_with(ANY, Path('path1'))
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list') @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list')
@patch('openlp.plugins.images.lib.mediaitem.Settings') @patch('openlp.plugins.images.lib.mediaitem.Settings')
@ -75,7 +75,7 @@ class TestImageMediaItem(TestCase):
Test that the validate_and_load_test() method when called with a group Test that the validate_and_load_test() method when called with a group
""" """
# GIVEN: A list of files # GIVEN: A list of files
file_list = ['/path1/image1.jpg', '/path2/image2.jpg'] file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
# WHEN: Calling validate_and_load with the list of files and a group # WHEN: Calling validate_and_load with the list of files and a group
self.media_item.validate_and_load(file_list, 'group') self.media_item.validate_and_load(file_list, 'group')
@ -83,7 +83,7 @@ class TestImageMediaItem(TestCase):
# THEN: load_list should have been called with the file list and the group name, # THEN: load_list should have been called with the file list and the group name,
# the directory should have been saved to the settings # the directory should have been saved to the settings
mocked_load_list.assert_called_once_with(file_list, 'group') mocked_load_list.assert_called_once_with(file_list, 'group')
mocked_settings().setValue.assert_called_once_with(ANY, Path('/', 'path1')) mocked_settings().setValue.assert_called_once_with(ANY, Path('path1'))
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
def test_save_new_images_list_empty_list(self, mocked_load_full_list): def test_save_new_images_list_empty_list(self, mocked_load_full_list):
@ -107,8 +107,8 @@ class TestImageMediaItem(TestCase):
Test that the save_new_images_list() calls load_full_list() when reload_list is set to True Test that the save_new_images_list() calls load_full_list() when reload_list is set to True
""" """
# GIVEN: A list with 1 image and a mocked out manager # GIVEN: A list with 1 image and a mocked out manager
image_list = ['test_image.jpg'] image_list = [Path('test_image.jpg')]
ImageFilenames.filename = '' ImageFilenames.file_path = None
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with reload_list=True # WHEN: We run save_new_images_list with reload_list=True
@ -118,7 +118,7 @@ class TestImageMediaItem(TestCase):
self.assertEquals(mocked_load_full_list.call_count, 1, 'load_full_list() should have been called') self.assertEquals(mocked_load_full_list.call_count, 1, 'load_full_list() should have been called')
# CLEANUP: Remove added attribute from ImageFilenames # CLEANUP: Remove added attribute from ImageFilenames
delattr(ImageFilenames, 'filename') delattr(ImageFilenames, 'file_path')
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list') @patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
def test_save_new_images_list_single_image_without_reload(self, mocked_load_full_list): def test_save_new_images_list_single_image_without_reload(self, mocked_load_full_list):
@ -126,7 +126,7 @@ class TestImageMediaItem(TestCase):
Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False
""" """
# GIVEN: A list with 1 image and a mocked out manager # GIVEN: A list with 1 image and a mocked out manager
image_list = ['test_image.jpg'] image_list = [Path('test_image.jpg')]
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with reload_list=False # WHEN: We run save_new_images_list with reload_list=False
@ -141,7 +141,7 @@ class TestImageMediaItem(TestCase):
Test that the save_new_images_list() saves all images in the list Test that the save_new_images_list() saves all images in the list
""" """
# GIVEN: A list with 3 images # GIVEN: A list with 3 images
image_list = ['test_image_1.jpg', 'test_image_2.jpg', 'test_image_3.jpg'] image_list = [Path('test_image_1.jpg'), Path('test_image_2.jpg'), Path('test_image_3.jpg')]
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the list of 3 images # WHEN: We run save_new_images_list with the list of 3 images
@ -157,7 +157,7 @@ class TestImageMediaItem(TestCase):
Test that the save_new_images_list() ignores everything in the provided list except strings Test that the save_new_images_list() ignores everything in the provided list except strings
""" """
# GIVEN: A list with images and objects # GIVEN: A list with images and objects
image_list = ['test_image_1.jpg', None, True, ImageFilenames(), 'test_image_2.jpg'] image_list = [Path('test_image_1.jpg'), None, True, ImageFilenames(), Path('test_image_2.jpg')]
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the list of images and objects # WHEN: We run save_new_images_list with the list of images and objects
@ -191,7 +191,7 @@ class TestImageMediaItem(TestCase):
ImageGroups.parent_id = 1 ImageGroups.parent_id = 1
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect self.media_item.manager.get_all_objects.side_effect = self._recursively_delete_group_side_effect
self.media_item.service_path = '' self.media_item.service_path = Path()
test_group = ImageGroups() test_group = ImageGroups()
test_group.id = 1 test_group.id = 1
@ -215,13 +215,13 @@ class TestImageMediaItem(TestCase):
# Create some fake objects that should be removed # Create some fake objects that should be removed
returned_object1 = ImageFilenames() returned_object1 = ImageFilenames()
returned_object1.id = 1 returned_object1.id = 1
returned_object1.filename = '/tmp/test_file_1.jpg' returned_object1.file_path = Path('/', 'tmp', 'test_file_1.jpg')
returned_object2 = ImageFilenames() returned_object2 = ImageFilenames()
returned_object2.id = 2 returned_object2.id = 2
returned_object2.filename = '/tmp/test_file_2.jpg' returned_object2.file_path = Path('/', 'tmp', 'test_file_2.jpg')
returned_object3 = ImageFilenames() returned_object3 = ImageFilenames()
returned_object3.id = 3 returned_object3.id = 3
returned_object3.filename = '/tmp/test_file_3.jpg' returned_object3.file_path = Path('/', 'tmp', 'test_file_3.jpg')
return [returned_object1, returned_object2, returned_object3] return [returned_object1, returned_object2, returned_object3]
if args[1] == ImageGroups and args[2]: if args[1] == ImageGroups and args[2]:
# Change the parent_id that is matched so we don't get into an endless loop # Change the parent_id that is matched so we don't get into an endless loop
@ -243,9 +243,9 @@ class TestImageMediaItem(TestCase):
test_image = ImageFilenames() test_image = ImageFilenames()
test_image.id = 1 test_image.id = 1
test_image.group_id = 1 test_image.group_id = 1
test_image.filename = 'imagefile.png' test_image.file_path = Path('imagefile.png')
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
self.media_item.service_path = '' self.media_item.service_path = Path()
self.media_item.list_view = MagicMock() self.media_item.list_view = MagicMock()
mocked_row_item = MagicMock() mocked_row_item = MagicMock()
mocked_row_item.data.return_value = test_image mocked_row_item.data.return_value = test_image
@ -265,13 +265,13 @@ class TestImageMediaItem(TestCase):
# GIVEN: An ImageFilenames that already exists in the database # GIVEN: An ImageFilenames that already exists in the database
image_file = ImageFilenames() image_file = ImageFilenames()
image_file.id = 1 image_file.id = 1
image_file.filename = '/tmp/test_file_1.jpg' image_file.file_path = Path('/', 'tmp', 'test_file_1.jpg')
self.media_item.manager = MagicMock() self.media_item.manager = MagicMock()
self.media_item.manager.get_object_filtered.return_value = image_file self.media_item.manager.get_object_filtered.return_value = image_file
ImageFilenames.filename = '' ImageFilenames.file_path = None
# WHEN: create_item_from_id() is called # WHEN: create_item_from_id() is called
item = self.media_item.create_item_from_id(1) item = self.media_item.create_item_from_id('1')
# THEN: A QTreeWidgetItem should be created with the above model object as it's data # THEN: A QTreeWidgetItem should be created with the above model object as it's data
self.assertIsInstance(item, QtWidgets.QTreeWidgetItem) self.assertIsInstance(item, QtWidgets.QTreeWidgetItem)
@ -279,4 +279,4 @@ class TestImageMediaItem(TestCase):
item_data = item.data(0, QtCore.Qt.UserRole) item_data = item.data(0, QtCore.Qt.UserRole)
self.assertIsInstance(item_data, ImageFilenames) self.assertIsInstance(item_data, ImageFilenames)
self.assertEqual(1, item_data.id) self.assertEqual(1, item_data.id)
self.assertEqual('/tmp/test_file_1.jpg', item_data.filename) self.assertEqual(Path('/', 'tmp', 'test_file_1.jpg'), item_data.file_path)

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2017 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 #
###############################################################################
"""
This module contains tests for the lib submodule of the Images plugin.
"""
import os
import shutil
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import patch
from openlp.core.common import AppLocation, Settings
from openlp.core.common.path import Path
from openlp.core.lib.db import Manager
from openlp.plugins.images.lib import upgrade
from openlp.plugins.images.lib.db import ImageFilenames, init_schema
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import TEST_RESOURCES_PATH
__default_settings__ = {
'images/db type': 'sqlite',
'images/background color': '#000000',
}
class TestImageDBUpgrade(TestCase, TestMixin):
"""
Test that the image database is upgraded correctly
"""
def setUp(self):
self.build_settings()
Settings().extend_default_settings(__default_settings__)
self.tmp_folder = mkdtemp()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
self.destroy_settings()
# Ignore errors since windows can have problems with locked files
shutil.rmtree(self.tmp_folder, ignore_errors=True)
def test_image_filenames_table(self):
"""
Test that the ImageFilenames table is correctly upgraded to the latest version
"""
# GIVEN: An unversioned image database
temp_db_name = os.path.join(self.tmp_folder, 'image-v0.sqlite')
shutil.copyfile(os.path.join(TEST_RESOURCES_PATH, 'images', 'image-v0.sqlite'), temp_db_name)
with patch.object(AppLocation, 'get_data_path', return_value=Path('/', 'test', 'dir')):
# WHEN: Initalising the database manager
manager = Manager('images', init_schema, db_file_path=temp_db_name, upgrade_mod=upgrade)
# THEN: The database should have been upgraded and image_filenames.file_path should return Path objects
upgraded_results = manager.get_all_objects(ImageFilenames)
expected_result_data = {1: Path('/', 'test', 'image1.jpg'),
2: Path('/', 'test', 'dir', 'image2.jpg'),
3: Path('/', 'test', 'dir', 'subdir', 'image3.jpg')}
for result in upgraded_results:
self.assertEqual(expected_result_data[result.id], result.file_path)

View File

@ -24,13 +24,12 @@ Functional tests to test the Impress class and related methods.
""" """
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock from unittest.mock import MagicMock
import os
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from openlp.core.common import Settings from openlp.core.common import Settings
from openlp.plugins.presentations.lib.impresscontroller import \ from openlp.core.common.path import Path
ImpressController, ImpressDocument, TextType from openlp.plugins.presentations.lib.impresscontroller import ImpressController, ImpressDocument, TextType
from openlp.plugins.presentations.presentationplugin import __default_settings__ from openlp.plugins.presentations.presentationplugin import __default_settings__
from tests.utils.constants import TEST_RESOURCES_PATH from tests.utils.constants import TEST_RESOURCES_PATH
@ -82,7 +81,7 @@ class TestImpressDocument(TestCase):
mocked_plugin = MagicMock() mocked_plugin = MagicMock()
mocked_plugin.settings_section = 'presentations' mocked_plugin.settings_section = 'presentations'
Settings().extend_default_settings(__default_settings__) Settings().extend_default_settings(__default_settings__)
self.file_name = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') self.file_name = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
self.ppc = ImpressController(mocked_plugin) self.ppc = ImpressController(mocked_plugin)
self.doc = ImpressDocument(self.ppc, self.file_name) self.doc = ImpressDocument(self.ppc, self.file_name)

View File

@ -26,6 +26,7 @@ from unittest import TestCase
from unittest.mock import patch, MagicMock, call from unittest.mock import patch, MagicMock, call
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.core.common.path import Path
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -92,17 +93,18 @@ class TestMediaItem(TestCase, TestMixin):
""" """
# GIVEN: A mocked controller, and mocked os.path.getmtime # GIVEN: A mocked controller, and mocked os.path.getmtime
mocked_controller = MagicMock() mocked_controller = MagicMock()
mocked_doc = MagicMock() mocked_doc = MagicMock(**{'get_thumbnail_path.return_value': Path()})
mocked_controller.add_document.return_value = mocked_doc mocked_controller.add_document.return_value = mocked_doc
mocked_controller.supports = ['tmp'] mocked_controller.supports = ['tmp']
self.media_item.controllers = { self.media_item.controllers = {
'Mocked': mocked_controller 'Mocked': mocked_controller
} }
presentation_file = 'file.tmp'
with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime, \ thmub_path = MagicMock(st_mtime=100)
patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: file_path = MagicMock(st_mtime=400)
mocked_getmtime.side_effect = [100, 200] with patch.object(Path, 'stat', side_effect=[thmub_path, file_path]), \
mocked_exists.return_value = True patch.object(Path, 'exists', return_value=True):
presentation_file = Path('file.tmp')
# WHEN: calling clean_up_thumbnails # WHEN: calling clean_up_thumbnails
self.media_item.clean_up_thumbnails(presentation_file, True) self.media_item.clean_up_thumbnails(presentation_file, True)
@ -123,9 +125,8 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.controllers = { self.media_item.controllers = {
'Mocked': mocked_controller 'Mocked': mocked_controller
} }
presentation_file = 'file.tmp' presentation_file = Path('file.tmp')
with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists: with patch.object(Path, 'exists', return_value=False):
mocked_exists.return_value = False
# WHEN: calling clean_up_thumbnails # WHEN: calling clean_up_thumbnails
self.media_item.clean_up_thumbnails(presentation_file, True) self.media_item.clean_up_thumbnails(presentation_file, True)

View File

@ -32,6 +32,7 @@ from PyQt5 import QtCore, QtGui
from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument
from openlp.core.common import Settings from openlp.core.common import Settings
from openlp.core.common.path import Path
from openlp.core.lib import ScreenList from openlp.core.lib import ScreenList
from tests.utils.constants import TEST_RESOURCES_PATH from tests.utils.constants import TEST_RESOURCES_PATH
@ -66,8 +67,8 @@ class TestPdfController(TestCase, TestMixin):
self.desktop.screenGeometry.return_value = SCREEN['size'] self.desktop.screenGeometry.return_value = SCREEN['size']
self.screens = ScreenList.create(self.desktop) self.screens = ScreenList.create(self.desktop)
Settings().extend_default_settings(__default_settings__) Settings().extend_default_settings(__default_settings__)
self.temp_folder = mkdtemp() self.temp_folder = Path(mkdtemp())
self.thumbnail_folder = mkdtemp() self.thumbnail_folder = Path(mkdtemp())
self.mock_plugin = MagicMock() self.mock_plugin = MagicMock()
self.mock_plugin.settings_section = self.temp_folder self.mock_plugin.settings_section = self.temp_folder
@ -77,8 +78,8 @@ class TestPdfController(TestCase, TestMixin):
""" """
del self.screens del self.screens
self.destroy_settings() self.destroy_settings()
shutil.rmtree(self.thumbnail_folder) shutil.rmtree(str(self.thumbnail_folder))
shutil.rmtree(self.temp_folder) shutil.rmtree(str(self.temp_folder))
def test_constructor(self): def test_constructor(self):
""" """
@ -98,7 +99,7 @@ class TestPdfController(TestCase, TestMixin):
Test loading of a Pdf using the PdfController Test loading of a Pdf using the PdfController
""" """
# GIVEN: A Pdf-file # GIVEN: A Pdf-file
test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
# WHEN: The Pdf is loaded # WHEN: The Pdf is loaded
controller = PdfController(plugin=self.mock_plugin) controller = PdfController(plugin=self.mock_plugin)
@ -118,7 +119,7 @@ class TestPdfController(TestCase, TestMixin):
Test loading of a Pdf and check size of generate pictures Test loading of a Pdf and check size of generate pictures
""" """
# GIVEN: A Pdf-file # GIVEN: A Pdf-file
test_file = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf') test_file = Path(TEST_RESOURCES_PATH, 'presentations', 'pdf_test1.pdf')
# WHEN: The Pdf is loaded # WHEN: The Pdf is loaded
controller = PdfController(plugin=self.mock_plugin) controller = PdfController(plugin=self.mock_plugin)
@ -131,7 +132,7 @@ class TestPdfController(TestCase, TestMixin):
# THEN: The load should succeed and pictures should be created and have been scales to fit the screen # THEN: The load should succeed and pictures should be created and have been scales to fit the screen
self.assertTrue(loaded, 'The loading of the PDF should succeed.') self.assertTrue(loaded, 'The loading of the PDF should succeed.')
image = QtGui.QImage(os.path.join(self.temp_folder, 'pdf_test1.pdf', 'mainslide001.png')) image = QtGui.QImage(os.path.join(str(self.temp_folder), 'pdf_test1.pdf', 'mainslide001.png'))
# Based on the converter used the resolution will differ a bit # Based on the converter used the resolution will differ a bit
if controller.gsbin: if controller.gsbin:
self.assertEqual(760, image.height(), 'The height should be 760') self.assertEqual(760, image.height(), 'The height should be 760')

View File

@ -22,7 +22,6 @@
""" """
This module contains tests for the pptviewcontroller module of the Presentations plugin. This module contains tests for the pptviewcontroller module of the Presentations plugin.
""" """
import os
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase
@ -30,6 +29,7 @@ from unittest.mock import MagicMock, patch
from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController
from openlp.core.common import is_win from openlp.core.common import is_win
from openlp.core.common.path import Path
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
from tests.utils.constants import TEST_RESOURCES_PATH from tests.utils.constants import TEST_RESOURCES_PATH
@ -184,7 +184,7 @@ class TestPptviewDocument(TestCase):
""" """
# GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file
doc = PptviewDocument(self.mock_controller, self.mock_presentation) doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.pptx') doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.pptx')
doc.save_titles_and_notes = MagicMock() doc.save_titles_and_notes = MagicMock()
# WHEN reading the titles and notes # WHEN reading the titles and notes
@ -201,13 +201,13 @@ class TestPptviewDocument(TestCase):
""" """
# GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
with patch('builtins.open') as mocked_open, \ with patch('builtins.open') as mocked_open, \
patch('openlp.plugins.presentations.lib.pptviewcontroller.os.path.exists') as mocked_exists, \ patch.object(Path, 'exists') as mocked_path_exists, \
patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \ patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') as \
mocked_dir_exists: mocked_dir_exists:
mocked_exists.return_value = False mocked_path_exists.return_value = False
mocked_dir_exists.return_value = False mocked_dir_exists.return_value = False
doc = PptviewDocument(self.mock_controller, self.mock_presentation) doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = 'Idontexist.pptx' doc.file_path = Path('Idontexist.pptx')
doc.save_titles_and_notes = MagicMock() doc.save_titles_and_notes = MagicMock()
# WHEN: Reading the titles and notes # WHEN: Reading the titles and notes
@ -215,7 +215,7 @@ class TestPptviewDocument(TestCase):
# THEN: File existens should have been checked, and not have been opened. # THEN: File existens should have been checked, and not have been opened.
doc.save_titles_and_notes.assert_called_once_with(None, None) doc.save_titles_and_notes.assert_called_once_with(None, None)
mocked_exists.assert_any_call('Idontexist.pptx') mocked_path_exists.assert_called_with()
self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.') self.assertEqual(mocked_open.call_count, 0, 'There should be no calls to open a file.')
def test_create_titles_and_notes_invalid_file(self): def test_create_titles_and_notes_invalid_file(self):
@ -228,7 +228,7 @@ class TestPptviewDocument(TestCase):
mocked_is_zf.return_value = False mocked_is_zf.return_value = False
mocked_open.filesize = 10 mocked_open.filesize = 10
doc = PptviewDocument(self.mock_controller, self.mock_presentation) doc = PptviewDocument(self.mock_controller, self.mock_presentation)
doc.file_path = os.path.join(TEST_RESOURCES_PATH, 'presentations', 'test.ppt') doc.file_path = Path(TEST_RESOURCES_PATH, 'presentations', 'test.ppt')
doc.save_titles_and_notes = MagicMock() doc.save_titles_and_notes = MagicMock()
# WHEN: reading the titles and notes # WHEN: reading the titles and notes

View File

@ -23,9 +23,8 @@
Functional tests to test the PresentationController and PresentationDocument Functional tests to test the PresentationController and PresentationDocument
classes and related methods. classes and related methods.
""" """
import os
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, mock_open, patch from unittest.mock import MagicMock, call, patch
from openlp.core.common.path import Path from openlp.core.common.path import Path
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument
@ -67,23 +66,18 @@ class TestPresentationController(TestCase):
Test PresentationDocument.save_titles_and_notes method with two valid lists Test PresentationDocument.save_titles_and_notes method with two valid lists
""" """
# GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder # GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
mocked_open = mock_open() with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.write_text') as mocked_write_text, \
with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder: patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
titles = ['uno', 'dos'] titles = ['uno', 'dos']
notes = ['one', 'two'] notes = ['one', 'two']
# WHEN: calling save_titles_and_notes # WHEN: calling save_titles_and_notes
mocked_get_thumbnail_folder.return_value = 'test' mocked_get_thumbnail_folder.return_value = Path('test')
self.document.save_titles_and_notes(titles, notes) self.document.save_titles_and_notes(titles, notes)
# THEN: the last call to open should have been for slideNotes2.txt # THEN: the last call to open should have been for slideNotes2.txt
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), mode='wt', encoding='utf-8') self.assertEqual(mocked_write_text.call_count, 3, 'There should be exactly three files written')
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8') mocked_write_text.assert_has_calls([call('uno\ndos'), call('one'), call('two')])
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), mode='wt', encoding='utf-8')
self.assertEqual(mocked_open.call_count, 3, 'There should be exactly three files opened')
mocked_open().writelines.assert_called_once_with(['uno', 'dos'])
mocked_open().write.assert_any_call('one')
mocked_open().write.assert_any_call('two')
def test_save_titles_and_notes_with_None(self): def test_save_titles_and_notes_with_None(self):
""" """
@ -107,10 +101,11 @@ class TestPresentationController(TestCase):
""" """
# GIVEN: A mocked open, get_thumbnail_folder and exists # GIVEN: A mocked open, get_thumbnail_folder and exists
with patch('builtins.open', mock_open(read_data='uno\ndos\n')) as mocked_open, \ with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text',
return_value='uno\ndos\n') as mocked_read_text, \
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: patch('openlp.plugins.presentations.lib.presentationcontroller.Path.exists') as mocked_exists:
mocked_get_thumbnail_folder.return_value = 'test' mocked_get_thumbnail_folder.return_value = Path('test')
mocked_exists.return_value = True mocked_exists.return_value = True
# WHEN: calling get_titles_and_notes # WHEN: calling get_titles_and_notes
@ -121,45 +116,36 @@ class TestPresentationController(TestCase):
self.assertEqual(len(result_titles), 2, 'There should be two items in the titles') self.assertEqual(len(result_titles), 2, 'There should be two items in the titles')
self.assertIs(type(result_notes), list, 'result_notes should be of type list') self.assertIs(type(result_notes), list, 'result_notes should be of type list')
self.assertEqual(len(result_notes), 2, 'There should be two items in the notes') self.assertEqual(len(result_notes), 2, 'There should be two items in the notes')
self.assertEqual(mocked_open.call_count, 3, 'Three files should be opened') self.assertEqual(mocked_read_text.call_count, 3, 'Three files should be read')
mocked_open.assert_any_call(os.path.join('test', 'titles.txt'), encoding='utf-8')
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), encoding='utf-8')
mocked_open.assert_any_call(os.path.join('test', 'slideNotes2.txt'), encoding='utf-8')
self.assertEqual(mocked_exists.call_count, 3, 'Three files should have been checked')
def test_get_titles_and_notes_with_file_not_found(self): def test_get_titles_and_notes_with_file_not_found(self):
""" """
Test PresentationDocument.get_titles_and_notes method with file not found Test PresentationDocument.get_titles_and_notes method with file not found
""" """
# GIVEN: A mocked open, get_thumbnail_folder and exists # GIVEN: A mocked open, get_thumbnail_folder and exists
with patch('builtins.open') as mocked_open, \ with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: mocked_read_text.side_effect = FileNotFoundError()
mocked_get_thumbnail_folder.return_value = 'test' mocked_get_thumbnail_folder.return_value = Path('test')
mocked_exists.return_value = False
# WHEN: calling get_titles_and_notes # WHEN: calling get_titles_and_notes
result_titles, result_notes = self.document.get_titles_and_notes() result_titles, result_notes = self.document.get_titles_and_notes()
# THEN: it should return two empty lists # THEN: it should return two empty lists
self.assertIs(type(result_titles), list, 'result_titles should be of type list') self.assertIsInstance(result_titles, list, 'result_titles should be of type list')
self.assertEqual(len(result_titles), 0, 'there be no titles') self.assertEqual(len(result_titles), 0, 'there be no titles')
self.assertIs(type(result_notes), list, 'result_notes should be a list') self.assertIsInstance(result_notes, list, 'result_notes should be a list')
self.assertEqual(len(result_notes), 0, 'but the list should be empty') self.assertEqual(len(result_notes), 0, 'but the list should be empty')
self.assertEqual(mocked_open.call_count, 0, 'No calls to open files')
self.assertEqual(mocked_exists.call_count, 1, 'There should be one call to file exists')
def test_get_titles_and_notes_with_file_error(self): def test_get_titles_and_notes_with_file_error(self):
""" """
Test PresentationDocument.get_titles_and_notes method with file errors Test PresentationDocument.get_titles_and_notes method with file errors
""" """
# GIVEN: A mocked open, get_thumbnail_folder and exists # GIVEN: A mocked open, get_thumbnail_folder and exists
with patch('builtins.open') as mocked_open, \ with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \ patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists: mocked_read_text.side_effect = IOError()
mocked_get_thumbnail_folder.return_value = 'test' mocked_get_thumbnail_folder.return_value = Path('test')
mocked_exists.return_value = True
mocked_open.side_effect = IOError()
# WHEN: calling get_titles_and_notes # WHEN: calling get_titles_and_notes
result_titles, result_notes = self.document.get_titles_and_notes() result_titles, result_notes = self.document.get_titles_and_notes()
@ -180,18 +166,16 @@ class TestPresentationDocument(TestCase):
patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists') patch('openlp.plugins.presentations.lib.presentationcontroller.check_directory_exists')
self.get_thumbnail_folder_patcher = \ self.get_thumbnail_folder_patcher = \
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder') patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
self.os_patcher = patch('openlp.plugins.presentations.lib.presentationcontroller.os')
self._setup_patcher = \ self._setup_patcher = \
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup') patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
self.mock_check_directory_exists = self.check_directory_exists_patcher.start() self.mock_check_directory_exists = self.check_directory_exists_patcher.start()
self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start() self.mock_get_thumbnail_folder = self.get_thumbnail_folder_patcher.start()
self.mock_os = self.os_patcher.start()
self.mock_setup = self._setup_patcher.start() self.mock_setup = self._setup_patcher.start()
self.mock_controller = MagicMock() self.mock_controller = MagicMock()
self.mock_get_thumbnail_folder.return_value = 'returned/path/' self.mock_get_thumbnail_folder.return_value = Path('returned/path/')
def tearDown(self): def tearDown(self):
""" """
@ -199,7 +183,6 @@ class TestPresentationDocument(TestCase):
""" """
self.check_directory_exists_patcher.stop() self.check_directory_exists_patcher.stop()
self.get_thumbnail_folder_patcher.stop() self.get_thumbnail_folder_patcher.stop()
self.os_patcher.stop()
self._setup_patcher.stop() self._setup_patcher.stop()
def test_initialise_presentation_document(self): def test_initialise_presentation_document(self):
@ -227,7 +210,7 @@ class TestPresentationDocument(TestCase):
PresentationDocument(self.mock_controller, 'Name') PresentationDocument(self.mock_controller, 'Name')
# THEN: check_directory_exists should have been called with 'returned/path/' # THEN: check_directory_exists should have been called with 'returned/path/'
self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path')) self.mock_check_directory_exists.assert_called_once_with(Path('returned', 'path/'))
self._setup_patcher.start() self._setup_patcher.start()
@ -244,20 +227,3 @@ class TestPresentationDocument(TestCase):
# THEN: load_presentation should return false # THEN: load_presentation should return false
self.assertFalse(result, "PresentationDocument.load_presentation should return false.") self.assertFalse(result, "PresentationDocument.load_presentation should return false.")
def test_get_file_name(self):
"""
Test the PresentationDocument.get_file_name method.
"""
# GIVEN: A mocked os.path.split which returns a list, an instance of PresentationDocument and
# arbitary file_path.
self.mock_os.path.split.return_value = ['directory', 'file.ext']
instance = PresentationDocument(self.mock_controller, 'Name')
instance.file_path = 'filepath'
# WHEN: Calling get_file_name
result = instance.get_file_name()
# THEN: get_file_name should return 'file.ext'
self.assertEqual(result, 'file.ext')

View File

@ -24,11 +24,11 @@ Package to test the openlp.core.__init__ package.
""" """
import os import os
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from openlp.core import OpenLP, parse_options from openlp.core import OpenLP
from openlp.core.common import Settings from openlp.core.common import Settings
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -96,9 +96,9 @@ class TestInit(TestCase, TestMixin):
'build': 'bzr000' 'build': 'bzr000'
} }
Settings().setValue('core/application version', '2.2.0') Settings().setValue('core/application version', '2.2.0')
with patch('openlp.core.get_application_version') as mocked_get_application_version,\ with patch('openlp.core.get_version') as mocked_get_version,\
patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question:
mocked_get_application_version.return_value = MOCKED_VERSION mocked_get_version.return_value = MOCKED_VERSION
mocked_question.return_value = QtWidgets.QMessageBox.No mocked_question.return_value = QtWidgets.QMessageBox.No
# WHEN: We check if a backup should be created # WHEN: We check if a backup should be created
@ -122,9 +122,9 @@ class TestInit(TestCase, TestMixin):
Settings().setValue('core/application version', '2.0.5') Settings().setValue('core/application version', '2.0.5')
self.openlp.splash = MagicMock() self.openlp.splash = MagicMock()
self.openlp.splash.isVisible.return_value = True self.openlp.splash.isVisible.return_value = True
with patch('openlp.core.get_application_version') as mocked_get_application_version,\ with patch('openlp.core.get_version') as mocked_get_version, \
patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question:
mocked_get_application_version.return_value = MOCKED_VERSION mocked_get_version.return_value = MOCKED_VERSION
mocked_question.return_value = QtWidgets.QMessageBox.No mocked_question.return_value = QtWidgets.QMessageBox.No
# WHEN: We check if a backup should be created # WHEN: We check if a backup should be created

View File

@ -26,7 +26,8 @@ from unittest import TestCase
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from openlp.core.common import Registry, Settings from openlp.core.common import Registry, Settings
from openlp.core.ui import ThemeManager, ThemeForm, FileRenameForm from openlp.core.common.path import Path
from openlp.core.ui import ThemeManager
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -91,6 +92,23 @@ class TestThemeManager(TestCase, TestMixin):
assert self.theme_manager.thumb_path.startswith(self.theme_manager.path) is True, \ assert self.theme_manager.thumb_path.startswith(self.theme_manager.path) is True, \
'The thumb path and the main path should start with the same value' 'The thumb path and the main path should start with the same value'
def test_build_theme_path(self):
"""
Test the thememanager build_theme_path - basic test
"""
# GIVEN: A new a call to initialise
with patch('openlp.core.common.AppLocation.get_section_data_path', return_value=Path('test/path')):
Settings().setValue('themes/global theme', 'my_theme')
self.theme_manager.theme_form = MagicMock()
self.theme_manager.load_first_time_themes = MagicMock()
# WHEN: the build_theme_path is run
self.theme_manager.build_theme_path()
# THEN: The thumbnail path should be a sub path of the test path
self.assertEqual(self.theme_manager.thumb_path, Path('test/path/thumbnails'))
def test_click_on_new_theme(self): def test_click_on_new_theme(self):
""" """
Test the on_add_theme event handler is called by the UI Test the on_add_theme event handler is called by the UI
@ -109,17 +127,16 @@ class TestThemeManager(TestCase, TestMixin):
@patch('openlp.core.ui.themeform.ThemeForm._setup') @patch('openlp.core.ui.themeform.ThemeForm._setup')
@patch('openlp.core.ui.filerenameform.FileRenameForm._setup') @patch('openlp.core.ui.filerenameform.FileRenameForm._setup')
def test_bootstrap_post(self, mocked_theme_form, mocked_rename_form): def test_bootstrap_post(self, mocked_rename_form, mocked_theme_form):
""" """
Test the functions of bootstrap_post_setup are called. Test the functions of bootstrap_post_setup are called.
""" """
# GIVEN: # GIVEN:
self.theme_manager.load_themes = MagicMock() self.theme_manager.load_themes = MagicMock()
self.theme_manager.path = MagicMock() self.theme_manager.theme_path = MagicMock()
# WHEN: # WHEN:
self.theme_manager.bootstrap_post_set_up() self.theme_manager.bootstrap_post_set_up()
# THEN: # THEN:
self.assertEqual(self.theme_manager.path, self.theme_manager.theme_form.path)
self.assertEqual(1, self.theme_manager.load_themes.call_count, "load_themes should have been called once") self.assertEqual(1, self.theme_manager.load_themes.call_count, "load_themes should have been called once")

View File

@ -22,13 +22,15 @@
""" """
Package to test the openlp.plugin.bible.lib.https package. Package to test the openlp.plugin.bible.lib.https package.
""" """
from unittest import TestCase, skip import os
from unittest import TestCase, skipIf
from unittest.mock import MagicMock from unittest.mock import MagicMock
from openlp.core.common import Registry from openlp.core.common import Registry
from openlp.plugins.bibles.lib.importers.http import BGExtract, CWExtract, BSExtract from openlp.plugins.bibles.lib.importers.http import BGExtract, CWExtract, BSExtract
@skipIf(os.environ.get('JENKINS_URL'), 'Skip Bible HTTP tests to prevent Jenkins from being blacklisted')
class TestBibleHTTP(TestCase): class TestBibleHTTP(TestCase):
def setUp(self): def setUp(self):
@ -38,6 +40,7 @@ class TestBibleHTTP(TestCase):
Registry.create() Registry.create()
Registry().register('service_list', MagicMock()) Registry().register('service_list', MagicMock())
Registry().register('application', MagicMock()) Registry().register('application', MagicMock())
Registry().register('main_window', MagicMock())
def test_bible_gateway_extract_books(self): def test_bible_gateway_extract_books(self):
""" """

Binary file not shown.