mirror of https://gitlab.com/openlp/openlp.git
HEAD
This commit is contained in:
commit
13bfa1c2f7
|
@ -20,13 +20,18 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import sys
|
||||
"""
|
||||
The entrypoint for OpenLP
|
||||
"""
|
||||
import faulthandler
|
||||
import multiprocessing
|
||||
import sys
|
||||
|
||||
from openlp.core.common import is_win, is_macosx
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core import main
|
||||
|
||||
faulthandler.enable(open(str(AppLocation.get_directory(AppLocation.CacheDir) / 'error.log'), 'wb'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
logging and a plugin framework are contained within the openlp.core module.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
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, \
|
||||
check_directory_exists, is_macosx, is_win, translate
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.versionchecker import VersionThread, get_application_version
|
||||
from openlp.core.common.path import Path, copytree
|
||||
from openlp.core.version import check_for_update, get_version
|
||||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.resources import qInitResources
|
||||
from openlp.core.ui import SplashScreen
|
||||
|
@ -49,34 +46,14 @@ from openlp.core.ui.exceptionform import ExceptionForm
|
|||
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
|
||||
from openlp.core.ui.mainwindow import MainWindow
|
||||
from openlp.core.ui.style import get_application_stylesheet
|
||||
|
||||
|
||||
__all__ = ['OpenLP', 'main']
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
WIN_REPAIR_STYLESHEET = """
|
||||
QMainWindow::separator
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
QDockWidget::title
|
||||
{
|
||||
border: 1px solid palette(dark);
|
||||
padding-left: 5px;
|
||||
padding-top: 2px;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
QToolBar
|
||||
{
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
||||
"""
|
||||
|
@ -121,14 +98,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
QtCore.QCoreApplication.exit()
|
||||
sys.exit()
|
||||
# Correct stylesheet bugs
|
||||
application_stylesheet = ''
|
||||
if not Settings().value('advanced/alternate rows'):
|
||||
base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base)
|
||||
alternate_rows_repair_stylesheet = \
|
||||
'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
|
||||
application_stylesheet += alternate_rows_repair_stylesheet
|
||||
if is_win():
|
||||
application_stylesheet += WIN_REPAIR_STYLESHEET
|
||||
application_stylesheet = get_application_stylesheet()
|
||||
if application_stylesheet:
|
||||
self.setStyleSheet(application_stylesheet)
|
||||
can_show_splash = Settings().value('core/show splash')
|
||||
|
@ -155,8 +125,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
self.processEvents()
|
||||
if not has_run_wizard:
|
||||
self.main_window.first_time()
|
||||
version = VersionThread(self.main_window)
|
||||
version.start()
|
||||
if Settings().value('core/update check'):
|
||||
check_for_update(self.main_window)
|
||||
self.main_window.is_display_blank()
|
||||
self.main_window.app_startup()
|
||||
return self.exec()
|
||||
|
@ -181,25 +151,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
"""
|
||||
Check if the data folder path exists.
|
||||
"""
|
||||
data_folder_path = str(AppLocation.get_data_path())
|
||||
if not os.path.exists(data_folder_path):
|
||||
log.critical('Database was not found in: ' + data_folder_path)
|
||||
status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'),
|
||||
translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}'
|
||||
'\n\nThe location of the data folder was '
|
||||
'previously changed from the OpenLP\'s '
|
||||
'default location. If the data was stored on '
|
||||
'removable device, that device needs to be '
|
||||
'made available.\n\nYou may reset the data '
|
||||
'location back to the default location, '
|
||||
'or you can try to make the current location '
|
||||
'available.\n\nDo you want to reset to the '
|
||||
'default data location? If not, OpenLP will be '
|
||||
'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)
|
||||
data_folder_path = AppLocation.get_data_path()
|
||||
if not data_folder_path.exists():
|
||||
log.critical('Database was not found in: %s', data_folder_path)
|
||||
status = QtWidgets.QMessageBox.critical(
|
||||
None, translate('OpenLP', 'Data Directory Error'),
|
||||
translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}\n\nThe location of the data folder '
|
||||
'was previously changed from the OpenLP\'s default location. If the data was '
|
||||
'stored on removable device, that device needs to be made available.\n\nYou may '
|
||||
'reset the data location back to the default location, or you can try to make the '
|
||||
'current location available.\n\nDo you want to reset to the default data location? '
|
||||
'If not, OpenLP will be 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 answer was "No", return "True", it will shutdown OpenLP in def main
|
||||
log.info('User requested termination')
|
||||
|
@ -240,7 +205,7 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
:param can_show_splash: Should OpenLP show the splash screen
|
||||
"""
|
||||
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
|
||||
if not has_run_wizard:
|
||||
Settings().setValue('core/application version', openlp_version)
|
||||
|
@ -253,11 +218,11 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
|||
'a backup of the old data folder?'),
|
||||
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
|
||||
# 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")
|
||||
data_folder_backup_path = data_folder_path + '-' + timestamp
|
||||
data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp)
|
||||
try:
|
||||
shutil.copytree(data_folder_path, data_folder_backup_path)
|
||||
copytree(data_folder_path, data_folder_backup_path)
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
|
||||
translate('OpenLP', 'Backup of the data folder failed!'))
|
||||
|
@ -415,7 +380,7 @@ def main(args=None):
|
|||
Registry.create()
|
||||
Registry().register('application', application)
|
||||
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
|
||||
if application.is_already_running():
|
||||
sys.exit()
|
||||
|
|
|
@ -19,10 +19,11 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
Download and "install" the remote web client
|
||||
"""
|
||||
import os
|
||||
import zipfile
|
||||
import urllib.error
|
||||
from zipfile import ZipFile
|
||||
|
||||
from openlp.core.common import AppLocation, Registry
|
||||
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
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
|
@ -48,11 +49,10 @@ def download_sha256():
|
|||
"""
|
||||
user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
|
||||
try:
|
||||
web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'),
|
||||
header=('User-Agent', user_agent))
|
||||
except (urllib.error.URLError, ConnectionError) as err:
|
||||
web_config = get_web_page('https://get.openlp.org/webclient/download.cfg', headers={'User-Agent': user_agent})
|
||||
except ConnectionError:
|
||||
return False
|
||||
file_bits = web_config.read().decode('utf-8').split()
|
||||
file_bits = web_config.split()
|
||||
return file_bits[0], file_bits[2]
|
||||
|
||||
|
||||
|
@ -63,7 +63,7 @@ def download_and_check(callback=None):
|
|||
sha256, version = download_sha256()
|
||||
file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
|
||||
callback.setRange(0, file_size)
|
||||
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'),
|
||||
if url_get_file(callback, 'https://get.openlp.org/webclient/site.zip',
|
||||
AppLocation.get_section_data_path('remotes') / 'site.zip',
|
||||
sha256=sha256):
|
||||
deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip')
|
|
@ -21,18 +21,13 @@
|
|||
###############################################################################
|
||||
import logging
|
||||
|
||||
import os
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
|
||||
from openlp.core.common import AppLocation
|
||||
|
||||
|
||||
static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes')))
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir)
|
||||
remote_endpoint = Endpoint('remote', template_dir='remotes', static_dir='remotes')
|
||||
|
||||
|
||||
@remote_endpoint.route('{view}')
|
|
@ -23,7 +23,7 @@ import logging
|
|||
import json
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import register_endpoint, requires_auth
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.common import Registry
|
||||
|
||||
|
||||
|
|
|
@ -26,17 +26,24 @@ with OpenLP. It uses JSON to communicate with the remotes.
|
|||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from waitress import serve
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.api.http import application
|
||||
from openlp.core.common import RegistryMixin, RegistryProperties, OpenLPMixin, Settings, Registry
|
||||
from openlp.core.common import AppLocation, RegistryMixin, RegistryProperties, OpenLPMixin, \
|
||||
Settings, Registry, UiStrings, check_directory_exists
|
||||
from openlp.core.lib import translate
|
||||
|
||||
from openlp.core.api.deploy import download_and_check, download_sha256
|
||||
from openlp.core.api.poll import Poller
|
||||
from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
|
||||
from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
|
||||
from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
|
||||
from openlp.core.api.endpoint.remote import remote_endpoint
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -59,6 +66,7 @@ class HttpWorker(QtCore.QObject):
|
|||
"""
|
||||
address = Settings().value('api/ip address')
|
||||
port = Settings().value('api/port')
|
||||
Registry().execute('get_website_version')
|
||||
serve(application, host=address, port=port)
|
||||
|
||||
def stop(self):
|
||||
|
@ -79,11 +87,15 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
|
|||
self.worker.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.thread.start()
|
||||
Registry().register_function('download_website', self.first_time)
|
||||
Registry().register_function('get_website_version', self.website_version)
|
||||
Registry().set_flag('website_version', '0.0')
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
"""
|
||||
Register the poll return service and start the servers.
|
||||
"""
|
||||
self.initialise()
|
||||
self.poller = Poller()
|
||||
Registry().register('poller', self.poller)
|
||||
application.initialise()
|
||||
|
@ -95,3 +107,79 @@ class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
|
|||
register_endpoint(main_endpoint)
|
||||
register_endpoint(service_endpoint)
|
||||
register_endpoint(api_service_endpoint)
|
||||
register_endpoint(remote_endpoint)
|
||||
|
||||
@staticmethod
|
||||
def initialise():
|
||||
"""
|
||||
Create the internal file structure if it does not exist
|
||||
:return:
|
||||
"""
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'assets')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'images')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static' / 'index')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'templates')
|
||||
|
||||
def first_time(self):
|
||||
"""
|
||||
Import web site code if active
|
||||
"""
|
||||
self.application.process_events()
|
||||
progress = DownloadProgressDialog(self)
|
||||
progress.forceShow()
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
download_and_check(progress)
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
progress.close()
|
||||
self.application.process_events()
|
||||
Settings().setValue('remotes/download version', self.version)
|
||||
|
||||
def website_version(self):
|
||||
"""
|
||||
Download and save the website version and sha256
|
||||
:return: None
|
||||
"""
|
||||
sha256, self.version = download_sha256()
|
||||
Registry().set_flag('website_sha256', sha256)
|
||||
Registry().set_flag('website_version', self.version)
|
||||
|
||||
|
||||
class DownloadProgressDialog(QtWidgets.QProgressDialog):
|
||||
"""
|
||||
Local class to handle download display based and supporting httputils:get_web_page
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(DownloadProgressDialog, self).__init__(parent.main_window)
|
||||
self.parent = parent
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowTitle(translate('RemotePlugin', 'Importing Website'))
|
||||
self.setLabelText(UiStrings().StartingImport)
|
||||
self.setCancelButton(None)
|
||||
self.setRange(0, 1)
|
||||
self.setMinimumDuration(0)
|
||||
self.was_cancelled = False
|
||||
self.previous_size = 0
|
||||
|
||||
def _download_progress(self, count, block_size):
|
||||
"""
|
||||
Calculate and display the download progress.
|
||||
"""
|
||||
increment = (count * block_size) - self.previous_size
|
||||
self._increment_progress_bar(None, increment)
|
||||
self.previous_size = count * block_size
|
||||
|
||||
def _increment_progress_bar(self, status_text, increment=1):
|
||||
"""
|
||||
Update the wizard progress page.
|
||||
|
||||
:param status_text: Current status information to display.
|
||||
:param increment: The value to increment the progress bar by.
|
||||
"""
|
||||
if status_text:
|
||||
self.setText(status_text)
|
||||
if increment > 0:
|
||||
self.setValue(self.value() + increment)
|
||||
self.parent.application.process_events()
|
||||
|
|
|
@ -52,7 +52,7 @@ class Poller(RegistryProperties):
|
|||
'isSecure': Settings().value('api/authentication enabled'),
|
||||
'isAuthorised': False,
|
||||
'chordNotation': Settings().value('songs/chord notation'),
|
||||
'isStagedActive': self.is_stage_active(),
|
||||
'isStageActive': self.is_stage_active(),
|
||||
'isLiveActive': self.is_live_active(),
|
||||
'isChordsActive': self.is_chords_active()
|
||||
}
|
||||
|
|
|
@ -202,6 +202,8 @@ class ApiTab(SettingsTab):
|
|||
self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
|
||||
http_url_temp = http_url + 'stage'
|
||||
self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
|
||||
http_url_temp = http_url + 'chords'
|
||||
self.chords_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
|
||||
http_url_temp = http_url + 'main'
|
||||
self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@ import sys
|
|||
from openlp.core.common import Settings, is_win, is_macosx
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
|
||||
if not is_win() and not is_macosx():
|
||||
try:
|
||||
from xdg import BaseDirectory
|
||||
|
|
|
@ -24,18 +24,12 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
|
|||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.client import HTTPException
|
||||
from random import randint
|
||||
|
||||
import requests
|
||||
|
||||
from openlp.core.common import Registry, trace_error_handler
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
@ -69,33 +63,6 @@ CONNECTION_TIMEOUT = 30
|
|||
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():
|
||||
"""
|
||||
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]
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
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:
|
||||
return None
|
||||
# This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437
|
||||
opener = urllib.request.build_opener(HTTPRedirectHandlerFixed())
|
||||
urllib.request.install_opener(opener)
|
||||
req = urllib.request.Request(url)
|
||||
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])
|
||||
if not headers:
|
||||
headers = {}
|
||||
if 'user-agent' not in [key.lower() for key in headers.keys()]:
|
||||
headers['User-Agent'] = get_user_agent()
|
||||
log.debug('Downloading URL = %s' % url)
|
||||
retries = 0
|
||||
while retries <= CONNECTION_RETRIES:
|
||||
retries += 1
|
||||
time.sleep(0.1)
|
||||
while retries < CONNECTION_RETRIES:
|
||||
try:
|
||||
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
|
||||
log.debug('Downloaded page {text}'.format(text=page.geturl()))
|
||||
response = requests.get(url, headers=headers, proxies=proxies, timeout=float(CONNECTION_TIMEOUT))
|
||||
log.debug('Downloaded page {url}'.format(url=response.url))
|
||||
break
|
||||
except urllib.error.URLError as err:
|
||||
log.exception('URLError on {text}'.format(text=url))
|
||||
log.exception('URLError: {text}'.format(text=err.reason))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except socket.timeout:
|
||||
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 IOError:
|
||||
# For now, catch IOError. All requests errors inherit from IOError
|
||||
log.exception('Unable to connect to {url}'.format(url=url))
|
||||
response = None
|
||||
if retries >= CONNECTION_RETRIES:
|
||||
raise ConnectionError('Unable to connect to {url}, see log for details'.format(url=url))
|
||||
retries += 1
|
||||
except:
|
||||
# Don't know what's happening, so reraise the original
|
||||
log.exception('Unknown error when trying to connect to {url}'.format(url=url))
|
||||
raise
|
||||
if update_openlp:
|
||||
Registry().get('application').process_events()
|
||||
if not page:
|
||||
log.exception('{text} could not be downloaded'.format(text=url))
|
||||
if not response or not response.text:
|
||||
log.error('{url} could not be downloaded'.format(url=url))
|
||||
return None
|
||||
log.debug(page)
|
||||
return page
|
||||
return response.text
|
||||
|
||||
|
||||
def get_url_file_size(url):
|
||||
|
@ -192,81 +124,67 @@ def get_url_file_size(url):
|
|||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
|
||||
meta = site.info()
|
||||
return int(meta.get("Content-Length"))
|
||||
except urllib.error.URLError:
|
||||
response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
|
||||
return int(response.headers['Content-Length'])
|
||||
except IOError:
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
raise ConnectionError('Unable to download {url}'.format(url=url))
|
||||
else:
|
||||
retries += 1
|
||||
time.sleep(0.1)
|
||||
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
|
||||
point. Returns False on download error.
|
||||
|
||||
:param callback: the class which needs to be updated
|
||||
: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
|
||||
"""
|
||||
block_count = 0
|
||||
block_size = 4096
|
||||
retries = 0
|
||||
log.debug("url_get_file: " + url)
|
||||
while True:
|
||||
log.debug('url_get_file: %s', url)
|
||||
while retries < CONNECTION_RETRIES:
|
||||
try:
|
||||
filename = open(f_path, "wb")
|
||||
url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
|
||||
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)
|
||||
with file_path.open('wb') as saved_file:
|
||||
response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True)
|
||||
if sha256:
|
||||
hasher.update(data)
|
||||
block_count += 1
|
||||
callback._download_progress(block_count, block_size)
|
||||
filename.close()
|
||||
hasher = hashlib.sha256()
|
||||
# Download until finished or canceled.
|
||||
for chunk in response.iter_content(chunk_size=block_size):
|
||||
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:
|
||||
log.error('sha256 sums did not match for file: {file}'.format(file=f_path))
|
||||
os.remove(f_path)
|
||||
log.error('sha256 sums did not match for file %s, got %s, expected %s', file_path, hasher.hexdigest(),
|
||||
sha256)
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return False
|
||||
except (urllib.error.URLError, socket.timeout) as err:
|
||||
break
|
||||
except IOError:
|
||||
trace_error_handler(log)
|
||||
filename.close()
|
||||
os.remove(f_path)
|
||||
if retries > CONNECTION_RETRIES:
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return False
|
||||
else:
|
||||
retries += 1
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
break
|
||||
# Delete file if cancelled, it may be a partial file.
|
||||
if callback.was_cancelled:
|
||||
os.remove(f_path)
|
||||
if callback.was_cancelled and file_path.exists():
|
||||
file_path.unlink()
|
||||
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']
|
||||
|
|
|
@ -141,7 +141,7 @@ class LanguageManager(object):
|
|||
if reg_ex.exactMatch(qmf):
|
||||
name = '{regex}'.format(regex=reg_ex.cap(1))
|
||||
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
|
||||
def get_qm_list():
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import shutil
|
||||
from contextlib import suppress
|
||||
|
||||
from openlp.core.common import is_win
|
||||
|
@ -29,6 +30,121 @@ else:
|
|||
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):
|
||||
"""
|
||||
A utility function to convert a Path object or NoneType to a string equivalent.
|
||||
|
|
|
@ -143,7 +143,7 @@ class Registry(object):
|
|||
log.exception('Exception for function {function}'.format(function=function))
|
||||
else:
|
||||
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
|
||||
|
||||
def get_flag(self, key):
|
||||
|
|
|
@ -136,6 +136,7 @@ class Settings(QtCore.QSettings):
|
|||
'advanced/single click service preview': False,
|
||||
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
'advanced/search as type': True,
|
||||
'advanced/use_dark_style': False,
|
||||
'api/twelve hour': True,
|
||||
'api/port': 4316,
|
||||
'api/websocket port': 4317,
|
||||
|
@ -177,6 +178,7 @@ class Settings(QtCore.QSettings):
|
|||
'images/background color': '#000000',
|
||||
'media/players': 'system,webkit',
|
||||
'media/override player': QtCore.Qt.Unchecked,
|
||||
'remotes/download version': '0.0',
|
||||
'players/background color': '#000000',
|
||||
'servicemanager/last directory': None,
|
||||
'servicemanager/last file': None,
|
||||
|
|
|
@ -88,9 +88,6 @@ class UiStrings(object):
|
|||
self.Error = translate('OpenLP.Ui', 'Error')
|
||||
self.Export = translate('OpenLP.Ui', 'Export')
|
||||
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.Help = translate('OpenLP.Ui', 'Help')
|
||||
self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
|
||||
|
|
|
@ -32,6 +32,7 @@ import math
|
|||
from PyQt5 import QtCore, QtGui, Qt, QtWidgets
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
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
|
||||
QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
|
||||
|
||||
:param icon:
|
||||
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
|
||||
location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
|
||||
: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 path
|
||||
location like ``Path(/path/to/file.png)``. However, the **recommended** way is to specify a resource string.
|
||||
:return: The build icon.
|
||||
:rtype: QtGui.QIcon
|
||||
"""
|
||||
if isinstance(icon, QtGui.QIcon):
|
||||
return icon
|
||||
|
@ -136,6 +138,8 @@ def build_icon(icon):
|
|||
button_icon = QtGui.QIcon()
|
||||
if isinstance(icon, str):
|
||||
pix_map = QtGui.QPixmap(icon)
|
||||
elif isinstance(icon, Path):
|
||||
pix_map = QtGui.QPixmap(str(icon))
|
||||
elif isinstance(icon, QtGui.QImage):
|
||||
pix_map = QtGui.QPixmap.fromImage(icon)
|
||||
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,
|
||||
before checking the existence of the file.
|
||||
|
||||
:param file_path: The path to the file. The file **must** exist!
|
||||
:param thumb_path: The path to the thumb.
|
||||
:return: True, False if the image has changed since the thumb was created.
|
||||
:param openlp.core.common.path.Path file_path: The path to the file. The file **must** exist!
|
||||
:param openlp.core.common.path.Path thumb_path: The path to the thumb.
|
||||
: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
|
||||
image_date = os.stat(file_path).st_mtime
|
||||
thumb_date = os.stat(thumb_path).st_mtime
|
||||
image_date = file_path.stat().st_mtime
|
||||
thumb_date = thumb_path.stat().st_mtime
|
||||
return image_date <= thumb_date
|
||||
|
||||
|
||||
|
@ -606,35 +611,6 @@ def create_separated_list(string_list):
|
|||
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 .screen import ScreenList
|
||||
from .formattingtags import FormattingTags
|
||||
|
|
|
@ -23,12 +23,13 @@
|
|||
"""
|
||||
The :mod:`db` module provides the core database functionality for OpenLP
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from copy import copy
|
||||
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.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError, ProgrammingError
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, mapper
|
||||
|
@ -37,7 +38,8 @@ from sqlalchemy.pool import NullPool
|
|||
from alembic.migration import MigrationContext
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -133,9 +135,10 @@ def get_db_path(plugin_name, db_file_name=None):
|
|||
if db_file_name is None:
|
||||
return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(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:
|
||||
return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name),
|
||||
name=db_file_name)
|
||||
return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name), name=db_file_name)
|
||||
|
||||
|
||||
def handle_db_error(plugin_name, db_file_name):
|
||||
|
@ -200,6 +203,55 @@ class BaseModel(object):
|
|||
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):
|
||||
"""
|
||||
Upgrade a database.
|
||||
|
@ -208,7 +260,7 @@ def upgrade_db(url, upgrade):
|
|||
:param upgrade: The python module that contains the upgrade instructions.
|
||||
"""
|
||||
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)
|
||||
|
||||
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 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:
|
||||
db_file_path = AppLocation.get_section_data_path(plugin_name) / db_file_name
|
||||
db_file_path = db_file_path / db_file_name
|
||||
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)
|
||||
|
||||
|
||||
|
@ -284,30 +337,30 @@ class Manager(object):
|
|||
"""
|
||||
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
|
||||
not exist.
|
||||
|
||||
:param plugin_name: The name to setup paths and settings section names
|
||||
:param init_schema: The init_schema function for this database
|
||||
:param db_file_name: The upgrade_schema function for this database
|
||||
:param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name
|
||||
being used.
|
||||
:param openlp.core.common.path.Path db_file_path: The file name to use for this database. Defaults to None
|
||||
resulting in the plugin_name being used.
|
||||
:param upgrade_mod: The upgrade_schema function for this database
|
||||
"""
|
||||
self.is_dirty = False
|
||||
self.session = None
|
||||
self.db_url = None
|
||||
if db_file_name:
|
||||
if db_file_path:
|
||||
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:
|
||||
self.db_url = init_url(plugin_name)
|
||||
if upgrade_mod:
|
||||
try:
|
||||
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
|
||||
except (SQLAlchemyError, DBAPIError):
|
||||
handle_db_error(plugin_name, db_file_name)
|
||||
handle_db_error(plugin_name, str(db_file_path))
|
||||
return
|
||||
if db_ver > up_ver:
|
||||
critical_error_message_box(
|
||||
|
@ -322,7 +375,7 @@ class Manager(object):
|
|||
try:
|
||||
self.session = init_schema(self.db_url)
|
||||
except (SQLAlchemyError, DBAPIError):
|
||||
handle_db_error(plugin_name, db_file_name)
|
||||
handle_db_error(plugin_name, str(db_file_path))
|
||||
else:
|
||||
self.session = session
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"color": "#000000",
|
||||
"direction": "vertical",
|
||||
"end_color": "#000000",
|
||||
"filename": "",
|
||||
"filename": null,
|
||||
"start_color": "#000000",
|
||||
"type": "solid"
|
||||
},
|
||||
|
|
|
@ -359,10 +359,8 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||
:param files: The files to be loaded.
|
||||
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
||||
"""
|
||||
names = []
|
||||
full_list = []
|
||||
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))
|
||||
duplicates_found = False
|
||||
files_added = False
|
||||
|
|
|
@ -27,7 +27,7 @@ import logging
|
|||
from PyQt5 import QtCore
|
||||
|
||||
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__)
|
||||
|
||||
|
@ -139,7 +139,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
|
|||
if version:
|
||||
self.version = version
|
||||
else:
|
||||
self.version = get_application_version()['version']
|
||||
self.version = get_version()['version']
|
||||
self.settings_section = self.name
|
||||
self.icon = None
|
||||
self.media_item_class = media_item_class
|
||||
|
|
|
@ -26,6 +26,7 @@ from string import Template
|
|||
from PyQt5 import QtGui, QtCore, QtWebKitWidgets
|
||||
|
||||
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, \
|
||||
build_lyrics_format_css, build_lyrics_outline_css, build_chords_css
|
||||
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]
|
||||
# if No file do not update cache
|
||||
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))
|
||||
|
||||
def pre_render(self, override_theme_data=None):
|
||||
|
@ -207,8 +208,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
|||
service_item.raw_footer = FOOTER
|
||||
# if No file do not update cache
|
||||
if theme_data.background_filename:
|
||||
self.image_manager.add_image(
|
||||
theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
|
||||
self.image_manager.add_image(path_to_str(theme_data.background_filename),
|
||||
ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
|
||||
theme_data, main, footer = self.pre_render(theme_data)
|
||||
service_item.theme_data = theme_data
|
||||
service_item.main = main
|
||||
|
|
|
@ -22,13 +22,13 @@
|
|||
"""
|
||||
Provide the theme XML and handling functions for OpenLP v2 themes.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree, objectify
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -160,9 +160,8 @@ class Theme(object):
|
|||
# basic theme object with defaults
|
||||
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
|
||||
jsn = get_text_file_string(json_path)
|
||||
jsn = json.loads(jsn)
|
||||
self.expand_json(jsn)
|
||||
self.background_filename = ''
|
||||
self.load_theme(jsn)
|
||||
self.background_filename = None
|
||||
|
||||
def expand_json(self, var, prev=None):
|
||||
"""
|
||||
|
@ -174,8 +173,6 @@ class Theme(object):
|
|||
for key, value in var.items():
|
||||
if prev:
|
||||
key = prev + "_" + key
|
||||
else:
|
||||
key = key
|
||||
if isinstance(value, dict):
|
||||
self.expand_json(value, key)
|
||||
else:
|
||||
|
@ -185,13 +182,13 @@ class Theme(object):
|
|||
"""
|
||||
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_filename and path:
|
||||
self.theme_name = self.theme_name.strip()
|
||||
self.background_filename = self.background_filename.strip()
|
||||
self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
|
||||
self.background_filename = path / self.theme_name / self.background_filename
|
||||
|
||||
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_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.
|
||||
|
||||
: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)
|
||||
|
||||
def export_theme(self):
|
||||
def export_theme(self, theme_path=None):
|
||||
"""
|
||||
Loop through the fields and build a dictionary of them
|
||||
|
||||
|
@ -223,7 +225,9 @@ class Theme(object):
|
|||
theme_data = {}
|
||||
for attr, value in self.__dict__.items():
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -19,3 +19,37 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`openlp.core.threading` module contains some common threading code
|
||||
"""
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
||||
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.
|
||||
|
||||
: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.
|
||||
:param str prefix: A prefix to be applied to the attribute names.
|
||||
:param bool auto_start: Automatically start the thread. Defaults to True.
|
||||
"""
|
||||
# Set up attribute names
|
||||
thread_name = 'thread'
|
||||
worker_name = 'worker'
|
||||
if prefix:
|
||||
thread_name = '_'.join([prefix, thread_name])
|
||||
worker_name = '_'.join([prefix, worker_name])
|
||||
# Create the thread and add the thread and the worker to the parent
|
||||
thread = QtCore.QThread()
|
||||
setattr(parent, thread_name, thread)
|
||||
setattr(parent, worker_name, worker)
|
||||
# Move the worker into the thread's context
|
||||
worker.moveToThread(thread)
|
||||
# Connect slots and signals
|
||||
thread.started.connect(worker.start)
|
||||
worker.quit.connect(thread.quit)
|
||||
worker.quit.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
if auto_start:
|
||||
thread.start()
|
|
@ -26,7 +26,7 @@ import webbrowser
|
|||
|
||||
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 .aboutdialog import UiAboutDialog
|
||||
|
||||
|
@ -49,7 +49,7 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
|
|||
Set up the dialog. This method is mocked out in tests.
|
||||
"""
|
||||
self.setup_ui(self)
|
||||
application_version = get_application_version()
|
||||
application_version = get_version()
|
||||
about_text = self.about_text_edit.toPlainText()
|
||||
about_text = about_text.replace('<version>', application_version['version'])
|
||||
if application_version['build']:
|
||||
|
|
|
@ -22,9 +22,8 @@
|
|||
"""
|
||||
The :mod:`advancedtab` provides an advanced settings facility.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
@ -33,6 +32,7 @@ from openlp.core.common.languagemanager import format_time
|
|||
from openlp.core.common.path import path_to_str
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.ui.lib import PathEdit, PathType
|
||||
from openlp.core.ui.style import HAS_DARK_STYLE
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -110,8 +110,80 @@ class AdvancedTab(SettingsTab):
|
|||
self.enable_auto_close_check_box.setObjectName('enable_auto_close_check_box')
|
||||
self.ui_layout.addRow(self.enable_auto_close_check_box)
|
||||
self.left_layout.addWidget(self.ui_group_box)
|
||||
if HAS_DARK_STYLE:
|
||||
self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box)
|
||||
self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox')
|
||||
self.ui_layout.addRow(self.use_dark_style_checkbox)
|
||||
# Data Directory
|
||||
self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.data_directory_group_box.setObjectName('data_directory_group_box')
|
||||
self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box)
|
||||
self.data_directory_layout.setObjectName('data_directory_layout')
|
||||
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
||||
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
|
||||
default_path=AppLocation.get_directory(AppLocation.DataDir))
|
||||
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
|
||||
self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
|
||||
self.new_data_directory_has_files_label.setWordWrap(True)
|
||||
self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
|
||||
self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png'))
|
||||
self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout()
|
||||
self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout')
|
||||
self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box)
|
||||
self.data_directory_copy_check_box.setObjectName('data_directory_copy_check_box')
|
||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box)
|
||||
self.data_directory_copy_check_layout.addStretch()
|
||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button)
|
||||
self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
|
||||
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
|
||||
self.left_layout.addWidget(self.data_directory_group_box)
|
||||
# Hide mouse
|
||||
self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.hide_mouse_group_box.setObjectName('hide_mouse_group_box')
|
||||
self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box)
|
||||
self.hide_mouse_layout.setObjectName('hide_mouse_layout')
|
||||
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box)
|
||||
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
|
||||
self.hide_mouse_layout.addWidget(self.hide_mouse_check_box)
|
||||
self.right_layout.addWidget(self.hide_mouse_group_box)
|
||||
# Service Item Slide Limits
|
||||
self.slide_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.slide_group_box.setObjectName('slide_group_box')
|
||||
self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
|
||||
self.slide_layout.setObjectName('slide_layout')
|
||||
self.slide_label = QtWidgets.QLabel(self.slide_group_box)
|
||||
self.slide_label.setWordWrap(True)
|
||||
self.slide_layout.addWidget(self.slide_label)
|
||||
self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.end_slide_radio_button.setObjectName('end_slide_radio_button')
|
||||
self.slide_layout.addWidget(self.end_slide_radio_button)
|
||||
self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
|
||||
self.slide_layout.addWidget(self.wrap_slide_radio_button)
|
||||
self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.next_item_radio_button.setObjectName('next_item_radio_button')
|
||||
self.slide_layout.addWidget(self.next_item_radio_button)
|
||||
self.right_layout.addWidget(self.slide_group_box)
|
||||
# Display Workarounds
|
||||
self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.display_workaround_group_box.setObjectName('display_workaround_group_box')
|
||||
self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box)
|
||||
self.display_workaround_layout.setObjectName('display_workaround_layout')
|
||||
self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box')
|
||||
self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box)
|
||||
self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.x11_bypass_check_box.setObjectName('x11_bypass_check_box')
|
||||
self.display_workaround_layout.addWidget(self.x11_bypass_check_box)
|
||||
self.alternate_rows_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.alternate_rows_check_box.setObjectName('alternate_rows_check_box')
|
||||
self.display_workaround_layout.addWidget(self.alternate_rows_check_box)
|
||||
self.right_layout.addWidget(self.display_workaround_group_box)
|
||||
# Default service name
|
||||
self.service_name_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.service_name_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.service_name_group_box.setObjectName('service_name_group_box')
|
||||
self.service_name_layout = QtWidgets.QFormLayout(self.service_name_group_box)
|
||||
self.service_name_check_box = QtWidgets.QCheckBox(self.service_name_group_box)
|
||||
|
@ -148,77 +220,11 @@ class AdvancedTab(SettingsTab):
|
|||
self.service_name_example = QtWidgets.QLabel(self.service_name_group_box)
|
||||
self.service_name_example.setObjectName('service_name_example')
|
||||
self.service_name_layout.addRow(self.service_name_example_label, self.service_name_example)
|
||||
self.left_layout.addWidget(self.service_name_group_box)
|
||||
# Data Directory
|
||||
self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.data_directory_group_box.setObjectName('data_directory_group_box')
|
||||
self.data_directory_layout = QtWidgets.QFormLayout(self.data_directory_group_box)
|
||||
self.data_directory_layout.setObjectName('data_directory_layout')
|
||||
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
||||
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
|
||||
default_path=AppLocation.get_directory(AppLocation.DataDir))
|
||||
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
|
||||
self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
|
||||
self.new_data_directory_has_files_label.setWordWrap(True)
|
||||
self.data_directory_cancel_button = QtWidgets.QToolButton(self.data_directory_group_box)
|
||||
self.data_directory_cancel_button.setObjectName('data_directory_cancel_button')
|
||||
self.data_directory_cancel_button.setIcon(build_icon(':/general/general_delete.png'))
|
||||
self.data_directory_copy_check_layout = QtWidgets.QHBoxLayout()
|
||||
self.data_directory_copy_check_layout.setObjectName('data_directory_copy_check_layout')
|
||||
self.data_directory_copy_check_box = QtWidgets.QCheckBox(self.data_directory_group_box)
|
||||
self.data_directory_copy_check_box.setObjectName('data_directory_copy_check_box')
|
||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_copy_check_box)
|
||||
self.data_directory_copy_check_layout.addStretch()
|
||||
self.data_directory_copy_check_layout.addWidget(self.data_directory_cancel_button)
|
||||
self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
|
||||
self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
|
||||
self.left_layout.addWidget(self.data_directory_group_box)
|
||||
self.right_layout.addWidget(self.service_name_group_box)
|
||||
# After the last item on each side, add some spacing
|
||||
self.left_layout.addStretch()
|
||||
# Hide mouse
|
||||
self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.hide_mouse_group_box.setObjectName('hide_mouse_group_box')
|
||||
self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box)
|
||||
self.hide_mouse_layout.setObjectName('hide_mouse_layout')
|
||||
self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box)
|
||||
self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
|
||||
self.hide_mouse_layout.addWidget(self.hide_mouse_check_box)
|
||||
self.right_layout.addWidget(self.hide_mouse_group_box)
|
||||
# Service Item Slide Limits
|
||||
self.slide_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.slide_group_box.setObjectName('slide_group_box')
|
||||
self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
|
||||
self.slide_layout.setObjectName('slide_layout')
|
||||
self.slide_label = QtWidgets.QLabel(self.slide_group_box)
|
||||
self.slide_label.setWordWrap(True)
|
||||
self.slide_layout.addWidget(self.slide_label)
|
||||
self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.end_slide_radio_button.setObjectName('end_slide_radio_button')
|
||||
self.slide_layout.addWidget(self.end_slide_radio_button)
|
||||
self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
|
||||
self.slide_layout.addWidget(self.wrap_slide_radio_button)
|
||||
self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
|
||||
self.next_item_radio_button.setObjectName('next_item_radio_button')
|
||||
self.slide_layout.addWidget(self.next_item_radio_button)
|
||||
self.right_layout.addWidget(self.slide_group_box)
|
||||
# Display Workarounds
|
||||
self.display_workaround_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.display_workaround_group_box.setObjectName('display_workaround_group_box')
|
||||
self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box)
|
||||
self.display_workaround_layout.setObjectName('display_workaround_layout')
|
||||
self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box')
|
||||
self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box)
|
||||
self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.x11_bypass_check_box.setObjectName('x11_bypass_check_box')
|
||||
self.display_workaround_layout.addWidget(self.x11_bypass_check_box)
|
||||
self.alternate_rows_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.alternate_rows_check_box.setObjectName('alternate_rows_check_box')
|
||||
self.display_workaround_layout.addWidget(self.alternate_rows_check_box)
|
||||
self.right_layout.addWidget(self.display_workaround_group_box)
|
||||
self.right_layout.addStretch()
|
||||
# Set up all the connections and things
|
||||
self.should_update_service_name_example = False
|
||||
self.service_name_check_box.toggled.connect(self.service_name_check_box_toggled)
|
||||
self.service_name_day.currentIndexChanged.connect(self.on_service_name_day_changed)
|
||||
|
@ -283,6 +289,8 @@ class AdvancedTab(SettingsTab):
|
|||
'Auto-scroll the next slide to bottom'))
|
||||
self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Enable application exit confirmation'))
|
||||
if HAS_DARK_STYLE:
|
||||
self.use_dark_style_checkbox.setText(translate('OpenLP.AdvancedTab', 'Use dark style (needs restart)'))
|
||||
self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name'))
|
||||
self.service_name_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable default service name'))
|
||||
self.service_name_time_label.setText(translate('OpenLP.AdvancedTab', 'Date and Time:'))
|
||||
|
@ -350,6 +358,8 @@ class AdvancedTab(SettingsTab):
|
|||
if self.autoscroll_map[i] == autoscroll_value and i < self.autoscroll_combo_box.count():
|
||||
self.autoscroll_combo_box.setCurrentIndex(i)
|
||||
self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation'))
|
||||
if HAS_DARK_STYLE:
|
||||
self.use_dark_style_checkbox.setChecked(settings.value('use_dark_style'))
|
||||
self.hide_mouse_check_box.setChecked(settings.value('hide mouse'))
|
||||
self.service_name_day.setCurrentIndex(settings.value('default service day'))
|
||||
self.service_name_time.setTime(QtCore.QTime(settings.value('default service hour'),
|
||||
|
@ -421,6 +431,8 @@ class AdvancedTab(SettingsTab):
|
|||
self.settings_form.register_post_process('config_screen_changed')
|
||||
self.settings_form.register_post_process('slidecontroller_update_slide_limits')
|
||||
settings.setValue('search as type', self.is_search_as_you_type_enabled)
|
||||
if HAS_DARK_STYLE:
|
||||
settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked())
|
||||
settings.endGroup()
|
||||
|
||||
def on_search_as_type_check_box_changed(self, check_state):
|
||||
|
@ -492,24 +504,27 @@ class AdvancedTab(SettingsTab):
|
|||
self.service_name_edit.setText(UiStrings().DefaultServiceName)
|
||||
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.
|
||||
answer = QtWidgets.QMessageBox.question(self, translate('OpenLP.AdvancedTab', 'Confirm Data Directory Change'),
|
||||
translate('OpenLP.AdvancedTab', 'Are you sure you want to change the '
|
||||
'location of the OpenLP data directory to:\n\n{path}'
|
||||
'\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)
|
||||
if answer != QtWidgets.QMessageBox.Yes:
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
return
|
||||
# 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.
|
||||
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()
|
||||
|
||||
def on_data_directory_copy_check_box_toggled(self):
|
||||
|
@ -526,9 +541,10 @@ class AdvancedTab(SettingsTab):
|
|||
def check_data_overwrite(self, data_path):
|
||||
"""
|
||||
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 os.path.exists(test_path):
|
||||
if (data_path / 'songs').exists():
|
||||
self.data_exists = True
|
||||
# Check is they want to replace existing data.
|
||||
answer = QtWidgets.QMessageBox.warning(self,
|
||||
|
@ -537,7 +553,7 @@ class AdvancedTab(SettingsTab):
|
|||
'WARNING: \n\nThe location you have selected \n\n{path}'
|
||||
'\n\nappears to contain OpenLP data files. Do you wish to '
|
||||
'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.No),
|
||||
QtWidgets.QMessageBox.No)
|
||||
|
@ -559,7 +575,7 @@ class AdvancedTab(SettingsTab):
|
|||
"""
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
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.data_directory_copy_check_box.hide()
|
||||
self.data_directory_cancel_button.hide()
|
||||
|
|
|
@ -71,7 +71,7 @@ except ImportError:
|
|||
VLC_VERSION = '-'
|
||||
|
||||
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 .exceptiondialog import Ui_ExceptionDialog
|
||||
|
@ -110,7 +110,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
|||
"""
|
||||
Create an exception report.
|
||||
"""
|
||||
openlp_version = get_application_version()
|
||||
openlp_version = get_version()
|
||||
description = self.description_text_edit.toPlainText()
|
||||
traceback = self.exception_text_edit.toPlainText()
|
||||
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()
|
||||
report_text = self.report_text.format(version=opts['version'], description=opts['description'],
|
||||
traceback=opts['traceback'], libs=opts['libs'], system=opts['system'])
|
||||
filename = str(file_path)
|
||||
try:
|
||||
report_file = open(filename, 'w')
|
||||
try:
|
||||
with file_path.open('w') as report_file:
|
||||
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:
|
||||
log.exception('Failed to write crash report')
|
||||
finally:
|
||||
report_file.close()
|
||||
|
||||
def on_send_report_button_clicked(self):
|
||||
"""
|
||||
|
@ -219,7 +209,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
|||
translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
|
||||
Settings().value(self.settings_section + '/last directory'),
|
||||
'{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:
|
||||
self.file_attachment = str(file_path)
|
||||
|
||||
|
|
|
@ -181,22 +181,16 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
|||
self.application.process_events()
|
||||
try:
|
||||
web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'),
|
||||
header=('User-Agent', user_agent))
|
||||
except (urllib.error.URLError, ConnectionError) as err:
|
||||
msg = QtWidgets.QMessageBox()
|
||||
title = translate('OpenLP.FirstTimeWizard', 'Network Error')
|
||||
msg.setText('{title} {error}'.format(title=title,
|
||||
error=err.code if hasattr(err, 'code') else ''))
|
||||
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()
|
||||
headers={'User-Agent': user_agent})
|
||||
except ConnectionError:
|
||||
QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),
|
||||
translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '
|
||||
'to connect to retrieve initial configuration information'),
|
||||
QtWidgets.QMessageBox.Ok)
|
||||
web_config = False
|
||||
if web_config:
|
||||
files = web_config.read()
|
||||
try:
|
||||
self.config.read_string(files.decode())
|
||||
self.config.read_string(web_config)
|
||||
self.web = self.config.get('general', 'base url')
|
||||
self.songs_url = self.web + self.config.get('songs', 'directory') + '/'
|
||||
self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/'
|
||||
|
@ -563,7 +557,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
|||
filename, sha256 = item.data(QtCore.Qt.UserRole)
|
||||
self._increment_progress_bar(self.downloading.format(name=filename), 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),
|
||||
destination, sha256):
|
||||
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.previous_size = 0
|
||||
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):
|
||||
missed_files.append('Bible: {name}'.format(name=bible))
|
||||
bibles_iterator += 1
|
||||
|
@ -588,7 +582,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
|||
self._increment_progress_bar(self.downloading.format(name=theme), 0)
|
||||
self.previous_size = 0
|
||||
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):
|
||||
missed_files.append('Theme: {name}'.format(name=theme))
|
||||
if missed_files:
|
||||
|
|
|
@ -163,7 +163,6 @@ class GeneralTab(SettingsTab):
|
|||
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.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.right_layout.addWidget(self.startup_group_box)
|
||||
# Logo
|
||||
|
|
|
@ -22,8 +22,7 @@
|
|||
""" Patch the QFileDialog so it accepts and returns Path objects"""
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common.path import Path, path_to_str, str_to_path
|
||||
from openlp.core.lib import replace_params
|
||||
from openlp.core.common.path import Path, path_to_str, replace_params, str_to_path
|
||||
|
||||
|
||||
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
|
||||
|
||||
:type parent: QtWidgets.QWidget or None
|
||||
:type parent: QtWidgets.QWidget | None
|
||||
:type caption: str
|
||||
:type directory: openlp.core.common.path.Path
|
||||
: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),))
|
||||
|
||||
|
@ -51,13 +50,13 @@ class FileDialog(QtWidgets.QFileDialog):
|
|||
"""
|
||||
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 directory: openlp.core.common.path.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
: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),))
|
||||
|
||||
|
@ -72,13 +71,13 @@ class FileDialog(QtWidgets.QFileDialog):
|
|||
"""
|
||||
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 directory: openlp.core.common.path.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
: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),))
|
||||
|
||||
|
@ -94,13 +93,13 @@ class FileDialog(QtWidgets.QFileDialog):
|
|||
"""
|
||||
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 directory: openlp.core.common.path.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
: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),))
|
||||
|
||||
|
|
|
@ -310,7 +310,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties):
|
|||
"""
|
||||
folder_path = FileDialog.getExistingDirectory(
|
||||
self, title, Settings().value(self.plugin.settings_section + '/' + setting_name),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
FileDialog.ShowDirsOnly)
|
||||
if folder_path:
|
||||
editbox.setText(str(folder_path))
|
||||
Settings().setValue(self.plugin.settings_section + '/' + setting_name, folder_path)
|
||||
|
|
|
@ -346,7 +346,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
|||
if not hasattr(self, 'service_item'):
|
||||
return False
|
||||
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)
|
||||
# Update the preview frame.
|
||||
if self.is_live:
|
||||
|
@ -454,7 +454,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
|||
Registry().execute('video_background_replaced')
|
||||
self.override = {}
|
||||
# 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')
|
||||
self.override = {}
|
||||
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_filename:
|
||||
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:
|
||||
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,
|
||||
|
@ -488,7 +488,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
|||
path = os.path.join(str(AppLocation.get_section_data_path('themes')),
|
||||
self.service_item.theme_data.theme_name)
|
||||
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')
|
||||
self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True)
|
||||
self._hide_mouse()
|
||||
|
|
|
@ -39,8 +39,7 @@ from openlp.core.api.http import server
|
|||
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
|
||||
check_directory_exists, translate, is_win, is_macosx, add_actions
|
||||
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.versionchecker import get_application_version
|
||||
from openlp.core.common.path import Path, copyfile, path_to_str, str_to_path
|
||||
from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
|
||||
|
@ -52,30 +51,12 @@ from openlp.core.ui.projector.manager import ProjectorManager
|
|||
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
|
||||
from openlp.core.ui.lib.filedialog import FileDialog
|
||||
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
|
||||
from openlp.core.ui.style import PROGRESSBAR_STYLE, get_library_stylesheet
|
||||
from openlp.core.version import get_version
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MEDIA_MANAGER_STYLE = """
|
||||
::tab#media_tool_box {
|
||||
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 0 palette(button), stop: 1.0 palette(mid));
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
/* This is here to make the tabs on KDE with the Breeze theme work */
|
||||
::tab:selected {}
|
||||
"""
|
||||
|
||||
PROGRESSBAR_STYLE = """
|
||||
QProgressBar{
|
||||
height: 10px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class Ui_MainWindow(object):
|
||||
"""
|
||||
|
@ -155,7 +136,7 @@ class Ui_MainWindow(object):
|
|||
# Create the MediaManager
|
||||
self.media_manager_dock = OpenLPDockWidget(main_window, 'media_manager_dock',
|
||||
':/system/system_mediamanager.png')
|
||||
self.media_manager_dock.setStyleSheet(MEDIA_MANAGER_STYLE)
|
||||
self.media_manager_dock.setStyleSheet(get_library_stylesheet())
|
||||
# Create the media toolbox
|
||||
self.media_tool_box = QtWidgets.QToolBox(self.media_manager_dock)
|
||||
self.media_tool_box.setObjectName('media_tool_box')
|
||||
|
@ -488,7 +469,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
"""
|
||||
The main window.
|
||||
"""
|
||||
openlp_version_check = QtCore.pyqtSignal(QtCore.QVariant)
|
||||
log.info('MainWindow loaded')
|
||||
|
||||
def __init__(self):
|
||||
|
@ -497,6 +477,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
"""
|
||||
super(MainWindow, self).__init__()
|
||||
Registry().register('main_window', self)
|
||||
self.version_thread = None
|
||||
self.version_worker = None
|
||||
self.clipboard = self.application.clipboard()
|
||||
self.arguments = ''.join(self.application.args)
|
||||
# Set up settings sections for the main application (not for use by plugins).
|
||||
|
@ -562,7 +544,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
self.application.set_busy_cursor()
|
||||
# Simple message boxes
|
||||
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('bootstrap_post_set_up', self.bootstrap_post_set_up)
|
||||
# Reset the cursor
|
||||
|
@ -607,7 +588,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
if widget:
|
||||
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.
|
||||
Triggered by delay thread and cannot display popup.
|
||||
|
@ -617,7 +598,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
log.debug('version_notice')
|
||||
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 '
|
||||
'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)
|
||||
|
||||
def show(self):
|
||||
|
@ -848,12 +829,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
QtWidgets.QMessageBox.No)
|
||||
if answer == QtWidgets.QMessageBox.No:
|
||||
return
|
||||
import_file_name, filter_used = QtWidgets.QFileDialog.getOpenFileName(
|
||||
import_file_path, filter_used = FileDialog.getOpenFileName(
|
||||
self,
|
||||
translate('OpenLP.MainWindow', 'Import settings'),
|
||||
'',
|
||||
None,
|
||||
translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
|
||||
if not import_file_name:
|
||||
if import_file_path is None:
|
||||
return
|
||||
setting_sections = []
|
||||
# Add main sections.
|
||||
|
@ -871,12 +852,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
# Add plugin sections.
|
||||
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.
|
||||
temp_directory = os.path.join(str(gettempdir()), 'openlp')
|
||||
check_directory_exists(Path(temp_directory))
|
||||
temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
|
||||
shutil.copyfile(import_file_name, temp_config)
|
||||
temp_dir_path = Path(gettempdir(), 'openlp')
|
||||
check_directory_exists(temp_dir_path)
|
||||
temp_config_path = temp_dir_path / import_file_path.name
|
||||
copyfile(import_file_path, temp_config_path)
|
||||
settings = Settings()
|
||||
import_settings = Settings(temp_config, Settings.IniFormat)
|
||||
import_settings = Settings(str(temp_config_path), Settings.IniFormat)
|
||||
|
||||
log.info('hook upgrade_plugin_settings')
|
||||
self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
|
||||
|
@ -920,7 +901,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
settings.setValue('{key}'.format(key=section_key), value)
|
||||
now = datetime.now()
|
||||
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.endGroup()
|
||||
settings.sync()
|
||||
|
@ -1011,6 +992,25 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
if not self.application.is_event_loop_active:
|
||||
event.ignore()
|
||||
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 self.settings_imported:
|
||||
self.clean_up(False)
|
||||
|
@ -1332,12 +1332,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
if self.application:
|
||||
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):
|
||||
"""
|
||||
Set the flag to copy the data
|
||||
|
@ -1349,7 +1343,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
Change the data directory.
|
||||
"""
|
||||
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.
|
||||
self.application.set_busy_cursor()
|
||||
if self.copy_data:
|
||||
|
@ -1358,7 +1352,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
self.show_status_message(
|
||||
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - {path} '
|
||||
'- Please wait for copy to finish').format(path=self.new_data_path))
|
||||
dir_util.copy_tree(old_data_path, self.new_data_path)
|
||||
dir_util.copy_tree(str(old_data_path), str(self.new_data_path))
|
||||
log.info('Copy successful')
|
||||
except (IOError, os.error, DistutilsFileError) as why:
|
||||
self.application.set_normal_cursor()
|
||||
|
@ -1373,9 +1367,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
|||
log.info('No data copy requested')
|
||||
# Change the location of data directory in config file.
|
||||
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.
|
||||
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')
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
|
|
|
@ -366,16 +366,20 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
|||
"""
|
||||
return self._modified
|
||||
|
||||
def set_file_name(self, file_name):
|
||||
def set_file_name(self, file_path):
|
||||
"""
|
||||
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())
|
||||
Settings().setValue('servicemanager/last file', Path(file_name))
|
||||
self._save_lite = self._file_name.endswith('.oszl')
|
||||
Settings().setValue('servicemanager/last file', file_path)
|
||||
if file_path and file_path.suffix == '.oszl':
|
||||
self._save_lite = True
|
||||
else:
|
||||
self._save_lite = False
|
||||
|
||||
def file_name(self):
|
||||
"""
|
||||
|
@ -474,7 +478,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
|||
"""
|
||||
self.service_manager_list.clear()
|
||||
self.service_items = []
|
||||
self.set_file_name('')
|
||||
self.set_file_name(None)
|
||||
self.service_id += 1
|
||||
self.set_modified(False)
|
||||
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)
|
||||
else:
|
||||
default_file_name = ''
|
||||
directory = path_to_str(Settings().value(self.main_window.service_manager_settings_section + '/last directory'))
|
||||
path = os.path.join(directory, default_file_name)
|
||||
default_file_path = Path(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
|
||||
# the long term.
|
||||
if self._file_name.endswith('oszl') or self.service_has_all_original_files:
|
||||
file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.main_window, UiStrings().SaveService, path,
|
||||
file_path, filter_used = FileDialog.getSaveFileName(
|
||||
self.main_window, UiStrings().SaveService, default_file_path,
|
||||
translate('OpenLP.ServiceManager',
|
||||
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
|
||||
else:
|
||||
file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.main_window, UiStrings().SaveService, path,
|
||||
file_path, filter_used = FileDialog.getSaveFileName(
|
||||
self.main_window, UiStrings().SaveService, file_path,
|
||||
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
|
||||
if not file_name:
|
||||
if not file_path:
|
||||
return False
|
||||
if os.path.splitext(file_name)[1] == '':
|
||||
file_name += '.osz'
|
||||
else:
|
||||
ext = os.path.splitext(file_name)[1]
|
||||
file_name.replace(ext, '.osz')
|
||||
self.set_file_name(file_name)
|
||||
file_path.with_suffix('.osz')
|
||||
self.set_file_name(file_path)
|
||||
self.decide_save_method()
|
||||
|
||||
def decide_save_method(self, field=None):
|
||||
|
@ -772,7 +774,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
|||
return
|
||||
file_to.close()
|
||||
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.process_service_items(items)
|
||||
delete_file(Path(p_file))
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
# -*- 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:`~openlp.core.ui.dark` module looks for and loads a dark theme
|
||||
"""
|
||||
from PyQt5 import QtGui
|
||||
|
||||
from openlp.core.common import is_macosx, is_win
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.common.settings import Settings
|
||||
|
||||
try:
|
||||
import qdarkstyle
|
||||
HAS_DARK_STYLE = True
|
||||
except ImportError:
|
||||
HAS_DARK_STYLE = False
|
||||
|
||||
WIN_REPAIR_STYLESHEET = """
|
||||
QMainWindow::separator
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
||||
QDockWidget::title
|
||||
{
|
||||
border: 1px solid palette(dark);
|
||||
padding-left: 5px;
|
||||
padding-top: 2px;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
QToolBar
|
||||
{
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
"""
|
||||
|
||||
MEDIA_MANAGER_STYLE = """
|
||||
::tab#media_tool_box {
|
||||
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 0 palette(button), stop: 1.0 palette(mid));
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
/* This is here to make the tabs on KDE with the Breeze theme work */
|
||||
::tab:selected {}
|
||||
"""
|
||||
|
||||
PROGRESSBAR_STYLE = """
|
||||
QProgressBar{
|
||||
height: 10px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_application_stylesheet():
|
||||
"""
|
||||
Return the correct application stylesheet based on the current style and operating system
|
||||
|
||||
:return str: The correct stylesheet as a string
|
||||
"""
|
||||
stylesheet = ''
|
||||
if HAS_DARK_STYLE and Settings().value('advanced/use_dark_style'):
|
||||
stylesheet = qdarkstyle.load_stylesheet_pyqt5()
|
||||
else:
|
||||
if not Settings().value('advanced/alternate rows'):
|
||||
base_color = Registry().get('application').palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base)
|
||||
alternate_rows_repair_stylesheet = \
|
||||
'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
|
||||
stylesheet += alternate_rows_repair_stylesheet
|
||||
if is_win():
|
||||
stylesheet += WIN_REPAIR_STYLESHEET
|
||||
return stylesheet
|
||||
|
||||
|
||||
def get_library_stylesheet():
|
||||
"""
|
||||
Return the correct stylesheet for the main window
|
||||
|
||||
:return str: The correct stylesheet as a string
|
||||
"""
|
||||
if not HAS_DARK_STYLE or not Settings().value('advanced/use_dark_style'):
|
||||
return MEDIA_MANAGER_STYLE
|
||||
else:
|
||||
return ''
|
|
@ -28,7 +28,6 @@ import os
|
|||
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.path import Path, path_to_str, str_to_path
|
||||
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui import ThemeLayoutForm
|
||||
|
@ -61,7 +60,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.setupUi(self)
|
||||
self.registerFields()
|
||||
self.update_theme_allowed = True
|
||||
self.temp_background_filename = ''
|
||||
self.temp_background_filename = None
|
||||
self.theme_layout_form = ThemeLayoutForm(self)
|
||||
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)
|
||||
|
@ -188,8 +187,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
"""
|
||||
background_image = BackgroundType.to_string(BackgroundType.Image)
|
||||
if self.page(self.currentId()) == self.background_page and \
|
||||
self.theme.background_type == background_image and \
|
||||
is_not_image_file(Path(self.theme.background_filename)):
|
||||
self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename):
|
||||
QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'),
|
||||
translate('OpenLP.ThemeWizard', 'You have not selected a '
|
||||
'background image. Please select one before continuing.'))
|
||||
|
@ -273,7 +271,7 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
Run the wizard.
|
||||
"""
|
||||
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.set_defaults()
|
||||
self.update_theme_allowed = True
|
||||
|
@ -318,11 +316,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.setField('background_type', 1)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
|
||||
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)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||
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)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
||||
self.setField('background_type', 3)
|
||||
|
@ -402,14 +400,14 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
|||
self.theme.background_type = BackgroundType.to_string(index)
|
||||
if self.theme.background_type != BackgroundType.to_string(BackgroundType.Image) 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.theme.background_filename = ''
|
||||
self.theme.background_filename = None
|
||||
if (self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or
|
||||
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.temp_background_filename = ''
|
||||
self.temp_background_filename = None
|
||||
self.set_background_page_values()
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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', 'Invalid theme name. Please enter one.'))
|
||||
return
|
||||
save_from = None
|
||||
save_to = None
|
||||
source_path = None
|
||||
destination_path = None
|
||||
if self.theme.background_type == BackgroundType.to_string(BackgroundType.Image) or \
|
||||
self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||
filename = os.path.split(str(self.theme.background_filename))[1]
|
||||
save_to = os.path.join(self.path, self.theme.theme_name, filename)
|
||||
save_from = self.theme.background_filename
|
||||
file_name = self.theme.background_filename.name
|
||||
destination_path = self.path / self.theme.theme_name / file_name
|
||||
source_path = self.theme.background_filename
|
||||
if not self.edit_mode and not self.theme_manager.check_if_theme_exists(self.theme.theme_name):
|
||||
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)
|
||||
|
|
|
@ -24,14 +24,14 @@ The Theme Manager manages adding, deleteing and modifying of themes.
|
|||
"""
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
|
||||
from xml.etree.ElementTree import ElementTree, XML
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
|
||||
UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file
|
||||
from openlp.core.common.path import Path, path_to_str, str_to_path
|
||||
UiStrings, check_directory_exists, translate, delete_file
|
||||
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, \
|
||||
check_item_selected, create_thumb, validate_thumb
|
||||
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.lib import OpenLPToolbar
|
||||
from openlp.core.ui.lib.filedialog import FileDialog
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
|
||||
|
||||
class Ui_ThemeManager(object):
|
||||
|
@ -135,7 +134,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
|||
self.settings_section = 'themes'
|
||||
# Variables
|
||||
self.theme_list = []
|
||||
self.old_background_image = None
|
||||
self.old_background_image_path = None
|
||||
|
||||
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.build_theme_path()
|
||||
self.load_first_time_themes()
|
||||
self.upgrade_themes()
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
"""
|
||||
process the bootstrap post setup request
|
||||
"""
|
||||
self.theme_form = ThemeForm(self)
|
||||
self.theme_form.path = self.path
|
||||
self.theme_form.path = self.theme_path
|
||||
self.file_rename_form = FileRenameForm()
|
||||
Registry().register_function('theme_update_global', self.change_global_from_tab)
|
||||
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):
|
||||
"""
|
||||
Set up the theme path variables
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
self.path = str(AppLocation.get_section_data_path(self.settings_section))
|
||||
check_directory_exists(Path(self.path))
|
||||
self.thumb_path = os.path.join(self.path, 'thumbnails')
|
||||
check_directory_exists(Path(self.thumb_path))
|
||||
self.theme_path = AppLocation.get_section_data_path(self.settings_section)
|
||||
check_directory_exists(self.theme_path)
|
||||
self.thumb_path = self.theme_path / 'thumbnails'
|
||||
check_directory_exists(self.thumb_path)
|
||||
|
||||
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.
|
||||
|
||||
:param theme_data: The theme to be used
|
||||
:param new_theme_name: The new theme name to save the data to
|
||||
:param Theme theme_data: The theme to be used
|
||||
:param str new_theme_name: The new theme name of the theme
|
||||
:rtype: None
|
||||
"""
|
||||
save_to = None
|
||||
save_from = None
|
||||
destination_path = None
|
||||
source_path = None
|
||||
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])
|
||||
save_from = theme_data.background_filename
|
||||
destination_path = self.theme_path / new_theme_name / theme_data.background_filename.name
|
||||
source_path = theme_data.background_filename
|
||||
theme_data.theme_name = new_theme_name
|
||||
theme_data.extend_image_filename(self.path)
|
||||
self.save_theme(theme_data, save_from, save_to)
|
||||
theme_data.extend_image_filename(self.theme_path)
|
||||
self.save_theme(theme_data, source_path, destination_path)
|
||||
self.load_themes()
|
||||
|
||||
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()
|
||||
theme = self.get_theme_data(item.data(QtCore.Qt.UserRole))
|
||||
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.exec(True)
|
||||
self.old_background_image = None
|
||||
self.old_background_image_path = None
|
||||
self.renderer.update_theme(theme.theme_name)
|
||||
self.load_themes()
|
||||
|
||||
|
@ -355,77 +371,76 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
|||
"""
|
||||
self.theme_list.remove(theme)
|
||||
thumb = '{name}.png'.format(name=theme)
|
||||
delete_file(Path(self.path, thumb))
|
||||
delete_file(Path(self.thumb_path, thumb))
|
||||
delete_file(self.theme_path / thumb)
|
||||
delete_file(self.thumb_path / thumb)
|
||||
try:
|
||||
# Windows is always unicode, so no need to encode filenames
|
||||
if is_win():
|
||||
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
|
||||
rmtree(self.theme_path / theme)
|
||||
except OSError:
|
||||
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
|
||||
:param field:
|
||||
Export the theme to a zip file
|
||||
|
||||
:param bool checked: Sent by the QAction.triggered signal. It's not used in this method.
|
||||
:rtype: None
|
||||
"""
|
||||
item = self.theme_list_widget.currentItem()
|
||||
if item is None:
|
||||
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
|
||||
return
|
||||
theme = item.data(QtCore.Qt.UserRole)
|
||||
theme_name = item.data(QtCore.Qt.UserRole)
|
||||
export_path, filter_used = \
|
||||
FileDialog.getSaveFileName(self.main_window,
|
||||
translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
|
||||
format(name=theme),
|
||||
translate('OpenLP.ThemeManager',
|
||||
'Save Theme - ({name})').format(name=theme_name),
|
||||
Settings().value(self.settings_section + '/last directory export'),
|
||||
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'),
|
||||
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
|
||||
self.application.set_busy_cursor()
|
||||
if export_path:
|
||||
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,
|
||||
translate('OpenLP.ThemeManager', 'Theme Exported'),
|
||||
translate('OpenLP.ThemeManager',
|
||||
'Your theme has been successfully exported.'))
|
||||
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.
|
||||
: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:
|
||||
theme_zip = zipfile.ZipFile(theme_path, 'w')
|
||||
source = os.path.join(self.path, theme)
|
||||
for files in os.walk(source):
|
||||
for name in files[2]:
|
||||
theme_zip.write(os.path.join(source, name), os.path.join(theme, name))
|
||||
theme_zip.close()
|
||||
with zipfile.ZipFile(str(theme_path), 'w') as theme_zip:
|
||||
source_path = self.theme_path / theme_name
|
||||
for file_path in source_path.iterdir():
|
||||
theme_zip.write(str(file_path), os.path.join(theme_name, file_path.name))
|
||||
return True
|
||||
except OSError as ose:
|
||||
self.log_exception('Export Theme Failed')
|
||||
critical_error_message_box(translate('OpenLP.ThemeManager', 'Theme Export Failed'),
|
||||
translate('OpenLP.ThemeManager', 'The theme export failed because this error '
|
||||
'occurred: {err}').format(err=ose.strerror))
|
||||
if theme_zip:
|
||||
theme_zip.close()
|
||||
shutil.rmtree(theme_path, True)
|
||||
translate('OpenLP.ThemeManager',
|
||||
'The theme_name export failed because this error occurred: {err}')
|
||||
.format(err=ose.strerror))
|
||||
if theme_path.exists():
|
||||
rmtree(theme_path, True)
|
||||
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
|
||||
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,
|
||||
translate('OpenLP.ThemeManager', 'Select Theme Import File'),
|
||||
Settings().value(self.settings_section + '/last directory import'),
|
||||
|
@ -435,8 +450,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
|||
return
|
||||
self.application.set_busy_cursor()
|
||||
for file_path in file_paths:
|
||||
self.unzip_theme(path_to_str(file_path), self.path)
|
||||
Settings().setValue(self.settings_section + '/last directory import', file_path)
|
||||
self.unzip_theme(file_path, self.theme_path)
|
||||
Settings().setValue(self.settings_section + '/last directory import', file_path.parent)
|
||||
self.load_themes()
|
||||
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
|
||||
"""
|
||||
self.application.set_busy_cursor()
|
||||
files = AppLocation.get_files(self.settings_section, '.otz')
|
||||
for theme_file in files:
|
||||
theme_file = os.path.join(self.path, str(theme_file))
|
||||
self.unzip_theme(theme_file, self.path)
|
||||
delete_file(Path(theme_file))
|
||||
files = AppLocation.get_files(self.settings_section, '.png')
|
||||
theme_paths = AppLocation.get_files(self.settings_section, '.otz')
|
||||
for theme_path in theme_paths:
|
||||
theme_path = self.theme_path / theme_path
|
||||
self.unzip_theme(theme_path, self.theme_path)
|
||||
delete_file(theme_path)
|
||||
theme_paths = AppLocation.get_files(self.settings_section, '.png')
|
||||
# No themes have been found so create one
|
||||
if not files:
|
||||
if not theme_paths:
|
||||
theme = Theme()
|
||||
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)
|
||||
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
|
||||
files.sort(key=lambda file_name: get_locale_key(str(file_name)))
|
||||
# now process the file list of png files
|
||||
for name in files:
|
||||
name = str(name)
|
||||
for file in files:
|
||||
# check to see file is in theme root directory
|
||||
theme = os.path.join(self.path, name)
|
||||
if os.path.exists(theme):
|
||||
text_name = os.path.splitext(name)[0]
|
||||
theme_path = self.theme_path / file
|
||||
if theme_path.exists():
|
||||
text_name = theme_path.stem
|
||||
if text_name == self.global_theme:
|
||||
name = translate('OpenLP.ThemeManager', '{name} (default)').format(name=text_name)
|
||||
else:
|
||||
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)
|
||||
if validate_thumb(theme, thumb):
|
||||
if validate_thumb(theme_path, thumb):
|
||||
icon = build_icon(thumb)
|
||||
else:
|
||||
icon = create_thumb(theme, thumb)
|
||||
icon = create_thumb(str(theme_path), str(thumb))
|
||||
item_name.setIcon(icon)
|
||||
item_name.setData(QtCore.Qt.UserRole, text_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):
|
||||
"""
|
||||
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
|
||||
:return: The theme object.
|
||||
:param str theme_name: Name of the theme to load from file
|
||||
:return: The theme object.
|
||||
:rtype: Theme
|
||||
"""
|
||||
self.log_debug('get theme data for theme {name}'.format(name=theme_name))
|
||||
theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name))
|
||||
theme_name = str(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)
|
||||
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:
|
||||
self.log_debug('No theme data - using default theme')
|
||||
return Theme()
|
||||
else:
|
||||
if jsn:
|
||||
return self._create_theme_from_json(theme_data, self.path)
|
||||
else:
|
||||
return self._create_theme_from_xml(theme_data, self.path)
|
||||
return self._create_theme_from_json(theme_data, self.theme_path)
|
||||
|
||||
def over_write_message_box(self, theme_name):
|
||||
"""
|
||||
|
@ -543,172 +549,148 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
|||
defaultButton=QtWidgets.QMessageBox.No)
|
||||
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
|
||||
and upgrade if necessary.
|
||||
:param file_name:
|
||||
:param directory:
|
||||
:param openlp.core.common.path.Path file_path:
|
||||
:param openlp.core.common.path.Path directory_path:
|
||||
"""
|
||||
self.log_debug('Unzipping theme {name}'.format(name=file_name))
|
||||
theme_zip = None
|
||||
out_file = None
|
||||
self.log_debug('Unzipping theme {name}'.format(name=file_path))
|
||||
file_xml = None
|
||||
abort_import = True
|
||||
json_theme = False
|
||||
theme_name = ""
|
||||
try:
|
||||
theme_zip = zipfile.ZipFile(file_name)
|
||||
json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
|
||||
if len(json_file) != 1:
|
||||
# TODO: remove XML handling at some point but would need a auto conversion to run first.
|
||||
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
|
||||
if len(xml_file) != 1:
|
||||
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
|
||||
raise ValidationError
|
||||
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
|
||||
theme_version = xml_tree.get('version', default=None)
|
||||
if not theme_version or float(theme_version) < 2.0:
|
||||
self.log_error('Theme version is less than 2.0')
|
||||
raise ValidationError
|
||||
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)
|
||||
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']
|
||||
if len(json_file) != 1:
|
||||
# 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']
|
||||
if len(xml_file) != 1:
|
||||
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
|
||||
raise ValidationError
|
||||
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
|
||||
theme_version = xml_tree.get('version', default=None)
|
||||
if not theme_version or float(theme_version) < 2.0:
|
||||
self.log_error('Theme version is less than 2.0')
|
||||
raise ValidationError
|
||||
theme_name = xml_tree.find('name').text.strip()
|
||||
else:
|
||||
out_file = open(full_name, 'wb')
|
||||
out_file.write(theme_zip.read(name))
|
||||
out_file.close()
|
||||
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 = 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):
|
||||
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
|
||||
except ValidationError:
|
||||
critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
|
||||
translate('OpenLP.ThemeManager', 'File is not a valid theme.'))
|
||||
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:
|
||||
# As all files are closed, we can create the Theme.
|
||||
if file_xml:
|
||||
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:
|
||||
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)
|
||||
# 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):
|
||||
"""
|
||||
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
|
||||
:rtype: bool
|
||||
"""
|
||||
theme_dir = os.path.join(self.path, theme_name)
|
||||
if os.path.exists(theme_dir):
|
||||
if (self.theme_path / theme_name).exists():
|
||||
critical_error_message_box(
|
||||
translate('OpenLP.ThemeManager', 'Validation Error'),
|
||||
translate('OpenLP.ThemeManager', 'A theme with this name already exists.'))
|
||||
return False
|
||||
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
|
||||
|
||||
:param theme: The theme data object.
|
||||
:param image_from: Where the theme image is currently located.
|
||||
:param image_to: Where the Theme Image is to be saved to
|
||||
:param Theme theme: The theme data object.
|
||||
:param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
|
||||
: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):
|
||||
self.image_manager.update_image_border(theme.background_filename,
|
||||
self.image_manager.update_image_border(path_to_str(theme.background_filename),
|
||||
ImageSource.Theme,
|
||||
QtGui.QColor(theme.background_border_color))
|
||||
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
|
||||
|
||||
:param theme: The theme data object.
|
||||
:param image_from: Where the theme image is currently located.
|
||||
:param image_to: Where the Theme Image is to be saved to
|
||||
:param Theme theme: The theme data object.
|
||||
:param openlp.core.common.path.Path image_source_path: Where the theme image is currently located.
|
||||
:param openlp.core.common.path.Path image_destination_path: Where the Theme Image is to be saved to
|
||||
:rtype: None
|
||||
"""
|
||||
name = theme.theme_name
|
||||
theme_pretty = theme.export_theme()
|
||||
theme_dir = os.path.join(self.path, name)
|
||||
check_directory_exists(Path(theme_dir))
|
||||
theme_file = os.path.join(theme_dir, name + '.json')
|
||||
if self.old_background_image and image_to != self.old_background_image:
|
||||
delete_file(Path(self.old_background_image))
|
||||
out_file = None
|
||||
theme_pretty = theme.export_theme(self.theme_path)
|
||||
theme_dir = self.theme_path / name
|
||||
check_directory_exists(theme_dir)
|
||||
theme_path = theme_dir / '{file_name}.json'.format(file_name=name)
|
||||
try:
|
||||
out_file = open(theme_file, 'w', encoding='utf-8')
|
||||
out_file.write(theme_pretty)
|
||||
theme_path.write_text(theme_pretty)
|
||||
except IOError:
|
||||
self.log_exception('Saving theme to file failed')
|
||||
finally:
|
||||
if out_file:
|
||||
out_file.close()
|
||||
if image_from and os.path.abspath(image_from) != os.path.abspath(image_to):
|
||||
try:
|
||||
# Windows is always unicode, so no need to encode filenames
|
||||
if is_win():
|
||||
shutil.copyfile(image_from, image_to)
|
||||
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')
|
||||
if image_source_path and image_destination_path:
|
||||
if self.old_background_image_path and image_destination_path != self.old_background_image_path:
|
||||
delete_file(self.old_background_image_path)
|
||||
if image_source_path != image_destination_path:
|
||||
try:
|
||||
copyfile(image_source_path, image_destination_path)
|
||||
except IOError:
|
||||
self.log_exception('Failed to save theme image')
|
||||
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
|
||||
|
||||
:param name: The name of the theme.
|
||||
:param str theme_name: The name of the theme.
|
||||
:param theme: The theme data object.
|
||||
"""
|
||||
frame = self.generate_image(theme)
|
||||
sample_path_name = os.path.join(self.path, name + '.png')
|
||||
if os.path.exists(sample_path_name):
|
||||
os.unlink(sample_path_name)
|
||||
frame.save(sample_path_name, 'png')
|
||||
thumb = os.path.join(self.thumb_path, '{name}.png'.format(name=name))
|
||||
create_thumb(sample_path_name, thumb, False)
|
||||
sample_path_name = self.theme_path / '{file_name}.png'.format(file_name=theme_name)
|
||||
if sample_path_name.exists():
|
||||
sample_path_name.unlink()
|
||||
frame.save(str(sample_path_name), 'png')
|
||||
thumb_path = self.thumb_path / '{name}.png'.format(name=theme_name)
|
||||
create_thumb(str(sample_path_name), str(thumb_path), False)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
def _create_theme_from_xml(theme_xml, image_path):
|
||||
"""
|
||||
Return a theme object using information parsed from XML
|
||||
|
||||
: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.
|
||||
:rtype: Theme
|
||||
"""
|
||||
theme = Theme()
|
||||
theme.parse(theme_xml)
|
||||
theme.extend_image_filename(image_path)
|
||||
return theme
|
||||
|
||||
@staticmethod
|
||||
def _create_theme_from_json(theme_json, image_path):
|
||||
def _create_theme_from_json(self, theme_json, image_path):
|
||||
"""
|
||||
Return a theme object using information parsed from JSON
|
||||
|
||||
: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.
|
||||
:rtype: Theme
|
||||
"""
|
||||
theme = Theme()
|
||||
theme.load_theme(theme_json)
|
||||
theme.load_theme(theme_json, self.theme_path)
|
||||
theme.extend_image_filename(image_path)
|
||||
return theme
|
||||
|
||||
|
|
|
@ -211,8 +211,8 @@ class ThemesTab(SettingsTab):
|
|||
"""
|
||||
Utility method to update the global theme preview image.
|
||||
"""
|
||||
image = self.theme_manager.get_preview_image(self.global_theme)
|
||||
preview = QtGui.QPixmap(str(image))
|
||||
image_path = self.theme_manager.theme_path / '{file_name}.png'.format(file_name=self.global_theme)
|
||||
preview = QtGui.QPixmap(str(image_path))
|
||||
if not preview.isNull():
|
||||
preview = preview.scaled(300, 255, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
self.default_list_view.setPixmap(preview)
|
||||
|
|
|
@ -20,24 +20,22 @@
|
|||
# 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 os
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from datetime import date
|
||||
from distutils.version import LooseVersion
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import requests
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, Registry, Settings
|
||||
from openlp.core.common.httputils import ping
|
||||
from openlp.core.common import AppLocation, Settings
|
||||
from openlp.core.threading import run_thread
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -46,42 +44,93 @@ CONNECTION_TIMEOUT = 30
|
|||
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.
|
||||
This is threaded so that it doesn't affect the loading time of OpenLP.
|
||||
A worker class to fetch the version of OpenLP from the website. This is run from within a thread so that it
|
||||
doesn't affect the loading time of OpenLP.
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
new_version = QtCore.pyqtSignal(dict)
|
||||
no_internet = QtCore.pyqtSignal()
|
||||
quit = QtCore.pyqtSignal()
|
||||
|
||||
:param main_window: The main window Object.
|
||||
def __init__(self, last_check_date, current_version):
|
||||
"""
|
||||
log.debug("VersionThread - Initialise")
|
||||
super(VersionThread, self).__init__(None)
|
||||
self.main_window = main_window
|
||||
Constructor for the version check worker.
|
||||
|
||||
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)
|
||||
log.debug('Version thread - run')
|
||||
found = ping("openlp.io")
|
||||
Registry().set_flag('internet_present', found)
|
||||
update_check = Settings().value('core/update check')
|
||||
if found:
|
||||
Registry().execute('get_website_version')
|
||||
if update_check:
|
||||
app_version = get_application_version()
|
||||
version = check_latest_version(app_version)
|
||||
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
|
||||
version2=LooseVersion(str(app_version['full']))))
|
||||
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
|
||||
self.main_window.openlp_version_check.emit('{version}'.format(version=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.
|
||||
"""
|
||||
log.debug('VersionWorker - Start')
|
||||
# I'm not entirely sure why this was here, I'm commenting it out until I hit the same scenario
|
||||
time.sleep(1)
|
||||
download_url = 'http://www.openlp.org/files/version.txt'
|
||||
if self.current_version['build']:
|
||||
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::
|
||||
|
||||
|
@ -150,55 +199,3 @@ def get_application_version():
|
|||
else:
|
||||
log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['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
|
|
@ -70,7 +70,7 @@ class AlertForm(QtWidgets.QDialog, Ui_AlertDialog):
|
|||
item_name = QtWidgets.QListWidgetItem(alert.text)
|
||||
item_name.setData(QtCore.Qt.UserRole, alert.id)
|
||||
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.alert_list_widget.setCurrentRow(self.alert_list_widget.row(item_name))
|
||||
|
||||
|
|
|
@ -32,9 +32,6 @@ class AlertsTab(SettingsTab):
|
|||
"""
|
||||
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):
|
||||
self.setObjectName('AlertsTab')
|
||||
super(AlertsTab, self).setupUi()
|
||||
|
|
|
@ -62,7 +62,7 @@ def bibles_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'bibles', log)
|
||||
return service(request, 'bibles', log)
|
||||
|
||||
|
||||
@api_bibles_endpoint.route('bibles/search')
|
||||
|
@ -95,6 +95,6 @@ def bibles_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'bibles', log)
|
||||
return search(request, 'bibles', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -93,7 +93,7 @@ class BGExtract(RegistryProperties):
|
|||
NAME = 'BibleGateway'
|
||||
|
||||
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
|
||||
socket.setdefaulttimeout(30)
|
||||
|
||||
|
@ -285,15 +285,10 @@ class BGExtract(RegistryProperties):
|
|||
log.debug('BGExtract.get_books_from_http("{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)
|
||||
page = get_web_page(reference_url)
|
||||
if not page:
|
||||
page_source = get_web_page(reference_url)
|
||||
if not page_source:
|
||||
send_error_message('download')
|
||||
return None
|
||||
page_source = page.read()
|
||||
try:
|
||||
page_source = str(page_source, 'utf8')
|
||||
except UnicodeDecodeError:
|
||||
page_source = str(page_source, 'cp1251')
|
||||
try:
|
||||
soup = BeautifulSoup(page_source, 'lxml')
|
||||
except Exception:
|
||||
|
@ -759,7 +754,7 @@ class HTTPBible(BibleImport, RegistryProperties):
|
|||
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.
|
||||
|
||||
|
@ -772,15 +767,15 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
|
|||
if not reference_url:
|
||||
return None
|
||||
try:
|
||||
page = get_web_page(reference_url, header, True)
|
||||
page_source = get_web_page(reference_url, headers, update_openlp=True)
|
||||
except Exception as e:
|
||||
page = None
|
||||
if not page:
|
||||
log.exception('Unable to download Bible %s, unknown exception occurred', reference_url)
|
||||
page_source = None
|
||||
if not page_source:
|
||||
send_error_message('download')
|
||||
return None
|
||||
page_source = page.read()
|
||||
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
|
||||
try:
|
||||
soup = BeautifulSoup(page_source, 'lxml')
|
||||
|
|
|
@ -62,7 +62,7 @@ def custom_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'custom', log)
|
||||
return service(request, 'custom', log)
|
||||
|
||||
|
||||
@api_custom_endpoint.route('custom/search')
|
||||
|
@ -95,6 +95,6 @@ def custom_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'custom', log)
|
||||
return search(request, 'custom', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -34,9 +34,6 @@ class CustomTab(SettingsTab):
|
|||
"""
|
||||
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):
|
||||
self.setObjectName('CustomTab')
|
||||
super(CustomTab, self).setupUi()
|
||||
|
|
|
@ -75,7 +75,7 @@ def images_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'images', log)
|
||||
return service(request, 'images', log)
|
||||
|
||||
|
||||
@api_images_endpoint.route('images/search')
|
||||
|
@ -108,6 +108,6 @@ def images_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'images', log)
|
||||
return search(request, 'images', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -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.db import Manager
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -50,7 +50,7 @@ class ImagePlugin(Plugin):
|
|||
|
||||
def __init__(self):
|
||||
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.icon_path = ':/plugins/plugin_images.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
|
|
|
@ -22,11 +22,10 @@
|
|||
"""
|
||||
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.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):
|
||||
|
@ -65,7 +64,7 @@ def init_schema(url):
|
|||
|
||||
* id
|
||||
* group_id
|
||||
* filename
|
||||
* file_path
|
||||
"""
|
||||
session, metadata = init_db(url)
|
||||
|
||||
|
@ -80,7 +79,7 @@ def init_schema(url):
|
|||
image_filenames_table = Table('image_filenames', metadata,
|
||||
Column('id', types.Integer(), primary_key=True),
|
||||
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)
|
||||
|
|
|
@ -31,9 +31,6 @@ class ImageTab(SettingsTab):
|
|||
"""
|
||||
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):
|
||||
self.setObjectName('ImagesTab')
|
||||
super(ImageTab, self).setupUi()
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
@ -99,11 +98,11 @@ class ImageMediaItem(MediaManagerItem):
|
|||
self.list_view.setIconSize(QtCore.QSize(88, 50))
|
||||
self.list_view.setIndentation(self.list_view.default_indentation)
|
||||
self.list_view.allow_internal_dnd = True
|
||||
self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
|
||||
check_directory_exists(Path(self.service_path))
|
||||
self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
|
||||
check_directory_exists(self.service_path)
|
||||
# Load images from the database
|
||||
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):
|
||||
"""
|
||||
|
@ -211,8 +210,8 @@ class ImageMediaItem(MediaManagerItem):
|
|||
"""
|
||||
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
|
||||
for image in images:
|
||||
delete_file(Path(self.service_path, os.path.split(image.filename)[1]))
|
||||
delete_file(Path(self.generate_thumbnail_path(image)))
|
||||
delete_file(self.service_path / image.file_path.name)
|
||||
delete_file(self.generate_thumbnail_path(image))
|
||||
self.manager.delete_object(ImageFilenames, image.id)
|
||||
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
|
||||
for group in image_groups:
|
||||
|
@ -234,8 +233,8 @@ class ImageMediaItem(MediaManagerItem):
|
|||
if row_item:
|
||||
item_data = row_item.data(0, QtCore.Qt.UserRole)
|
||||
if isinstance(item_data, ImageFilenames):
|
||||
delete_file(Path(self.service_path, row_item.text(0)))
|
||||
delete_file(Path(self.generate_thumbnail_path(item_data)))
|
||||
delete_file(self.service_path / row_item.text(0))
|
||||
delete_file(self.generate_thumbnail_path(item_data))
|
||||
if item_data.group_id == 0:
|
||||
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
|
||||
else:
|
||||
|
@ -326,17 +325,19 @@ class ImageMediaItem(MediaManagerItem):
|
|||
"""
|
||||
Generate a path to the thumbnail
|
||||
|
||||
:param image: An instance of ImageFileNames
|
||||
:return: A path to the thumbnail of type str
|
||||
:param openlp.plugins.images.lib.db.ImageFilenames image: The image to generate the thumbnail path for.
|
||||
:return: A path to the thumbnail
|
||||
:rtype: openlp.core.common.path.Path
|
||||
"""
|
||||
ext = os.path.splitext(image.filename)[1].lower()
|
||||
return os.path.join(self.service_path, '{}{}'.format(str(image.id), ext))
|
||||
ext = image.file_path.suffix.lower()
|
||||
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):
|
||||
"""
|
||||
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 open_group: ImageGroups object of the group that must be expanded after reloading the list in the
|
||||
interface.
|
||||
|
@ -352,34 +353,34 @@ class ImageMediaItem(MediaManagerItem):
|
|||
self.expand_group(open_group.id)
|
||||
# Sort the images by its filename considering language specific.
|
||||
# characters.
|
||||
images.sort(key=lambda image_object: get_locale_key(os.path.split(str(image_object.filename))[1]))
|
||||
for image_file in images:
|
||||
log.debug('Loading image: {name}'.format(name=image_file.filename))
|
||||
filename = os.path.split(image_file.filename)[1]
|
||||
thumb = self.generate_thumbnail_path(image_file)
|
||||
if not os.path.exists(image_file.filename):
|
||||
images.sort(key=lambda image_object: get_locale_key(image_object.file_path.name))
|
||||
for image in images:
|
||||
log.debug('Loading image: {name}'.format(name=image.file_path))
|
||||
file_name = image.file_path.name
|
||||
thumbnail_path = self.generate_thumbnail_path(image)
|
||||
if not image.file_path.exists():
|
||||
icon = build_icon(':/general/general_delete.png')
|
||||
else:
|
||||
if validate_thumb(image_file.filename, thumb):
|
||||
icon = build_icon(thumb)
|
||||
if validate_thumb(image.file_path, thumbnail_path):
|
||||
icon = build_icon(thumbnail_path)
|
||||
else:
|
||||
icon = create_thumb(image_file.filename, thumb)
|
||||
item_name = QtWidgets.QTreeWidgetItem([filename])
|
||||
item_name.setText(0, filename)
|
||||
icon = create_thumb(image.file_path, thumbnail_path)
|
||||
item_name = QtWidgets.QTreeWidgetItem([file_name])
|
||||
item_name.setText(0, file_name)
|
||||
item_name.setIcon(0, icon)
|
||||
item_name.setToolTip(0, image_file.filename)
|
||||
item_name.setData(0, QtCore.Qt.UserRole, image_file)
|
||||
if image_file.group_id == 0:
|
||||
item_name.setToolTip(0, str(image.file_path))
|
||||
item_name.setData(0, QtCore.Qt.UserRole, image)
|
||||
if image.group_id == 0:
|
||||
self.list_view.addTopLevelItem(item_name)
|
||||
else:
|
||||
group_items[image_file.group_id].addChild(item_name)
|
||||
group_items[image.group_id].addChild(item_name)
|
||||
if not initial_load:
|
||||
self.main_window.increment_progress_bar()
|
||||
if not initial_load:
|
||||
self.main_window.finished_progress_bar()
|
||||
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.
|
||||
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
|
||||
"""
|
||||
self.application.set_normal_cursor()
|
||||
self.load_list(files, target_group)
|
||||
last_dir = os.path.split(files[0])[0]
|
||||
Settings().setValue(self.settings_section + '/last directory', Path(last_dir))
|
||||
self.load_list(file_paths, target_group)
|
||||
last_dir = file_paths[0].parent
|
||||
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.
|
||||
|
||||
: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 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:
|
||||
self.choose_group_form.existing_radio_button.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.nogroup_radio_button.isChecked():
|
||||
# User chose 'No group'
|
||||
|
@ -461,33 +462,33 @@ class ImageMediaItem(MediaManagerItem):
|
|||
return
|
||||
# Initialize busy cursor and progress bar
|
||||
self.application.set_busy_cursor()
|
||||
self.main_window.display_progress_bar(len(images))
|
||||
# Save the new images in the database
|
||||
self.save_new_images_list(images, group_id=parent_group.id, reload_list=False)
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename),
|
||||
self.main_window.display_progress_bar(len(image_paths))
|
||||
# Save the new image_paths in the database
|
||||
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.file_path),
|
||||
initial_load=initial_load, open_group=parent_group)
|
||||
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.
|
||||
|
||||
: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 reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
|
||||
the new images
|
||||
"""
|
||||
for filename in images_list:
|
||||
if not isinstance(filename, str):
|
||||
for image_path in image_paths:
|
||||
if not isinstance(image_path, Path):
|
||||
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.group_id = group_id
|
||||
image_file.filename = str(filename)
|
||||
image_file.file_path = image_path
|
||||
self.manager.save_object(image_file)
|
||||
self.main_window.increment_progress_bar()
|
||||
if reload_list and images_list:
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename))
|
||||
if reload_list and image_paths:
|
||||
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path))
|
||||
|
||||
def dnd_move_internal(self, target):
|
||||
"""
|
||||
|
@ -581,8 +582,8 @@ class ImageMediaItem(MediaManagerItem):
|
|||
return False
|
||||
# Find missing files
|
||||
for image in images:
|
||||
if not os.path.exists(image.filename):
|
||||
missing_items_file_names.append(image.filename)
|
||||
if not image.file_path.exists():
|
||||
missing_items_file_names.append(str(image.file_path))
|
||||
# We cannot continue, as all images do not exist.
|
||||
if not images:
|
||||
if not remote:
|
||||
|
@ -601,9 +602,9 @@ class ImageMediaItem(MediaManagerItem):
|
|||
return False
|
||||
# Continue with the existing images.
|
||||
for image in images:
|
||||
name = os.path.split(image.filename)[1]
|
||||
thumbnail = self.generate_thumbnail_path(image)
|
||||
service_item.add_from_image(image.filename, name, background, thumbnail)
|
||||
name = image.file_path.name
|
||||
thumbnail_path = self.generate_thumbnail_path(image)
|
||||
service_item.add_from_image(str(image.file_path), name, background, str(thumbnail_path))
|
||||
return True
|
||||
|
||||
def check_group_exists(self, new_group):
|
||||
|
@ -640,7 +641,7 @@ class ImageMediaItem(MediaManagerItem):
|
|||
if not self.check_group_exists(new_group):
|
||||
if self.manager.save_object(new_group):
|
||||
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.fill_groups_combobox(self.choose_group_form.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):
|
||||
# Only continue when an image is selected.
|
||||
return
|
||||
filename = bitem.data(0, QtCore.Qt.UserRole).filename
|
||||
if os.path.exists(filename):
|
||||
if self.live_controller.display.direct_image(filename, background):
|
||||
file_path = bitem.data(0, QtCore.Qt.UserRole).file_path
|
||||
if file_path.exists():
|
||||
if self.live_controller.display.direct_image(str(file_path), background):
|
||||
self.reset_action.setVisible(True)
|
||||
else:
|
||||
critical_error_message_box(
|
||||
|
@ -687,22 +688,22 @@ class ImageMediaItem(MediaManagerItem):
|
|||
critical_error_message_box(
|
||||
UiStrings().LiveBGError,
|
||||
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):
|
||||
"""
|
||||
Perform a search on the image file names.
|
||||
|
||||
:param string: The glob to search for
|
||||
:param show_error: Unused.
|
||||
:param str string: The glob to search for
|
||||
:param bool show_error: Unused.
|
||||
"""
|
||||
files = self.manager.get_all_objects(
|
||||
ImageFilenames, filter_clause=ImageFilenames.filename.contains(string),
|
||||
order_by_ref=ImageFilenames.filename)
|
||||
ImageFilenames, filter_clause=ImageFilenames.file_path.contains(string),
|
||||
order_by_ref=ImageFilenames.file_path)
|
||||
results = []
|
||||
for file_object in files:
|
||||
filename = os.path.split(str(file_object.filename))[1]
|
||||
results.append([file_object.filename, filename])
|
||||
file_name = file_object.file_path.name
|
||||
results.append([str(file_object.file_path), file_name])
|
||||
return results
|
||||
|
||||
def create_item_from_id(self, item_id):
|
||||
|
@ -711,8 +712,9 @@ class ImageMediaItem(MediaManagerItem):
|
|||
|
||||
:param item_id: Id to make live
|
||||
"""
|
||||
item_id = Path(item_id)
|
||||
item = QtWidgets.QTreeWidgetItem()
|
||||
item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.filename == item_id)
|
||||
item.setText(0, os.path.basename(item_data.filename))
|
||||
item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.file_path == item_id)
|
||||
item.setText(0, item_data.file_path.name)
|
||||
item.setData(0, QtCore.Qt.UserRole, item_data)
|
||||
return item
|
||||
|
|
|
@ -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')
|
|
@ -62,7 +62,7 @@ def media_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'media', log)
|
||||
return service(request, 'media', log)
|
||||
|
||||
|
||||
@api_media_endpoint.route('media/search')
|
||||
|
@ -95,6 +95,6 @@ def media_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'media', log)
|
||||
return search(request, 'media', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -76,7 +76,7 @@ def presentations_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'presentations', log)
|
||||
return service(request, 'presentations', log)
|
||||
|
||||
|
||||
@api_presentations_endpoint.route('presentations/search')
|
||||
|
@ -109,6 +109,6 @@ def presentations_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'presentations', log)
|
||||
return search(request, 'presentations', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -32,11 +32,14 @@
|
|||
# http://nxsy.org/comparing-documents-with-openoffice-and-python
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from openlp.core.common import is_win, Registry, delete_file
|
||||
from openlp.core.common.path import Path
|
||||
from PyQt5 import QtCore
|
||||
|
||||
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():
|
||||
from win32com.client import Dispatch
|
||||
|
@ -55,14 +58,6 @@ else:
|
|||
except ImportError:
|
||||
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__)
|
||||
|
||||
|
||||
|
@ -203,12 +198,15 @@ class ImpressDocument(PresentationDocument):
|
|||
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.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: File path for the document to load
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('Init Presentation OpenOffice')
|
||||
super(ImpressDocument, self).__init__(controller, presentation)
|
||||
super().__init__(controller, document_path)
|
||||
self.document = None
|
||||
self.presentation = None
|
||||
self.control = None
|
||||
|
@ -225,10 +223,9 @@ class ImpressDocument(PresentationDocument):
|
|||
if desktop is None:
|
||||
self.controller.start_process()
|
||||
desktop = self.controller.get_com_desktop()
|
||||
url = 'file:///' + self.file_path.replace('\\', '/').replace(':', '|').replace(' ', '%20')
|
||||
else:
|
||||
desktop = self.controller.get_uno_desktop()
|
||||
url = uno.systemPathToFileUrl(self.file_path)
|
||||
url = self.file_path.as_uri()
|
||||
if desktop is None:
|
||||
return False
|
||||
self.desktop = desktop
|
||||
|
@ -254,11 +251,8 @@ class ImpressDocument(PresentationDocument):
|
|||
log.debug('create thumbnails OpenOffice')
|
||||
if self.check_thumbnails():
|
||||
return
|
||||
if is_win():
|
||||
thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \
|
||||
.replace(':', '|').replace(' ', '%20')
|
||||
else:
|
||||
thumb_dir_url = uno.systemPathToFileUrl(self.get_temp_folder())
|
||||
temp_folder_path = self.get_temp_folder()
|
||||
thumb_dir_url = temp_folder_path.as_uri()
|
||||
properties = []
|
||||
properties.append(self.create_property('FilterName', 'impress_png_Export'))
|
||||
properties = tuple(properties)
|
||||
|
@ -266,17 +260,17 @@ class ImpressDocument(PresentationDocument):
|
|||
pages = doc.getDrawPages()
|
||||
if not pages:
|
||||
return
|
||||
if not os.path.isdir(self.get_temp_folder()):
|
||||
os.makedirs(self.get_temp_folder())
|
||||
if not temp_folder_path.is_dir():
|
||||
temp_folder_path.mkdir(parents=True)
|
||||
for index in range(pages.getCount()):
|
||||
page = pages.getByIndex(index)
|
||||
doc.getCurrentController().setCurrentPage(page)
|
||||
url_path = '{path}/{name}.png'.format(path=thumb_dir_url, name=str(index + 1))
|
||||
path = os.path.join(self.get_temp_folder(), str(index + 1) + '.png')
|
||||
url_path = '{path}/{name:d}.png'.format(path=thumb_dir_url, name=index + 1)
|
||||
path = temp_folder_path / '{number:d}.png'.format(number=index + 1)
|
||||
try:
|
||||
doc.storeToURL(url_path, properties)
|
||||
self.convert_thumbnail(path, index + 1)
|
||||
delete_file(Path(path))
|
||||
delete_file(path)
|
||||
except ErrorCodeIOException as exception:
|
||||
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
|
||||
except:
|
||||
|
|
|
@ -19,15 +19,13 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, Settings, UiStrings, translate
|
||||
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,\
|
||||
build_icon, check_item_selected, create_thumb, validate_thumb
|
||||
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))
|
||||
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()
|
||||
|
||||
def populate_display_types(self):
|
||||
|
@ -152,54 +150,57 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
else:
|
||||
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
|
||||
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()
|
||||
titles = [file_path.name for file_path in current_list]
|
||||
file_paths = [str_to_path(filename) for filename in file_paths]
|
||||
current_paths = self.get_file_list()
|
||||
titles = [file_path.name for file_path in current_paths]
|
||||
self.application.set_busy_cursor()
|
||||
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.
|
||||
files.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1]))
|
||||
for file in files:
|
||||
file_paths.sort(key=lambda file_path: get_locale_key(file_path.name))
|
||||
for file_path in file_paths:
|
||||
if not initial_load:
|
||||
self.main_window.increment_progress_bar()
|
||||
if current_list.count(file) > 0:
|
||||
if current_paths.count(file_path) > 0:
|
||||
continue
|
||||
filename = os.path.split(file)[1]
|
||||
if not os.path.exists(file):
|
||||
item_name = QtWidgets.QListWidgetItem(filename)
|
||||
file_name = file_path.name
|
||||
if not file_path.exists():
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setIcon(build_icon(ERROR_IMAGE))
|
||||
item_name.setData(QtCore.Qt.UserRole, file)
|
||||
item_name.setToolTip(file)
|
||||
item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path))
|
||||
item_name.setToolTip(str(file_path))
|
||||
self.list_view.addItem(item_name)
|
||||
else:
|
||||
if titles.count(filename) > 0:
|
||||
if titles.count(file_name) > 0:
|
||||
if not initial_load:
|
||||
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'A presentation with that filename already exists.'))
|
||||
continue
|
||||
controller_name = self.find_controller_by_type(filename)
|
||||
controller_name = self.find_controller_by_type(file_path)
|
||||
if controller_name:
|
||||
controller = self.controllers[controller_name]
|
||||
doc = controller.add_document(file)
|
||||
thumb = os.path.join(doc.get_thumbnail_folder(), 'icon.png')
|
||||
preview = doc.get_thumbnail_path(1, True)
|
||||
if not preview and not initial_load:
|
||||
doc = controller.add_document(file_path)
|
||||
thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
|
||||
preview_path = doc.get_thumbnail_path(1, True)
|
||||
if not preview_path and not initial_load:
|
||||
doc.load_presentation()
|
||||
preview = doc.get_thumbnail_path(1, True)
|
||||
preview_path = doc.get_thumbnail_path(1, True)
|
||||
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')
|
||||
else:
|
||||
if validate_thumb(preview, thumb):
|
||||
icon = build_icon(thumb)
|
||||
if validate_thumb(Path(preview_path), Path(thumbnail_path)):
|
||||
icon = build_icon(thumbnail_path)
|
||||
else:
|
||||
icon = create_thumb(preview, thumb)
|
||||
icon = create_thumb(str(preview_path), str(thumbnail_path))
|
||||
else:
|
||||
if initial_load:
|
||||
icon = build_icon(':/general/general_delete.png')
|
||||
|
@ -208,10 +209,10 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
translate('PresentationPlugin.MediaItem',
|
||||
'This type of presentation is not supported.'))
|
||||
continue
|
||||
item_name = QtWidgets.QListWidgetItem(filename)
|
||||
item_name.setData(QtCore.Qt.UserRole, file)
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setData(QtCore.Qt.UserRole, path_to_str(file_path))
|
||||
item_name.setIcon(icon)
|
||||
item_name.setToolTip(file)
|
||||
item_name.setToolTip(str(file_path))
|
||||
self.list_view.addItem(item_name)
|
||||
if not initial_load:
|
||||
self.main_window.finished_progress_bar()
|
||||
|
@ -228,8 +229,8 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
self.application.set_busy_cursor()
|
||||
self.main_window.display_progress_bar(len(row_list))
|
||||
for item in items:
|
||||
filepath = str(item.data(QtCore.Qt.UserRole))
|
||||
self.clean_up_thumbnails(filepath)
|
||||
file_path = str_to_path(item.data(QtCore.Qt.UserRole))
|
||||
self.clean_up_thumbnails(file_path)
|
||||
self.main_window.increment_progress_bar()
|
||||
self.main_window.finished_progress_bar()
|
||||
for row in row_list:
|
||||
|
@ -237,30 +238,29 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
Settings().setValue(self.settings_section + '/presentations files', self.get_file_list())
|
||||
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
|
||||
|
||||
:param filepath: File path of the presention to clean up after
|
||||
:param clean_for_update: Only clean thumbnails if update is needed
|
||||
:return: None
|
||||
:param openlp.core.common.path.Path file_path: File path of the presention to clean up after
|
||||
:param bool clean_for_update: Only clean thumbnails if update is needed
|
||||
:rtype: None
|
||||
"""
|
||||
for cidx in self.controllers:
|
||||
root, file_ext = os.path.splitext(filepath)
|
||||
file_ext = file_ext[1:]
|
||||
file_ext = file_path.suffix[1:]
|
||||
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:
|
||||
thumb_path = doc.get_thumbnail_path(1, True)
|
||||
if not thumb_path or not os.path.exists(filepath) or os.path.getmtime(
|
||||
thumb_path) < os.path.getmtime(filepath):
|
||||
if not thumb_path or not file_path.exists() or \
|
||||
thumb_path.stat().st_mtime < file_path.stat().st_mtime:
|
||||
doc.presentation_deleted()
|
||||
else:
|
||||
doc.presentation_deleted()
|
||||
doc.close_presentation()
|
||||
|
||||
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.
|
||||
|
||||
|
@ -276,10 +276,9 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
items = self.list_view.selectedItems()
|
||||
if len(items) > 1:
|
||||
return False
|
||||
filename = presentation_file
|
||||
if filename is None:
|
||||
filename = items[0].data(QtCore.Qt.UserRole)
|
||||
file_type = os.path.splitext(filename.lower())[1][1:]
|
||||
if file_path is None:
|
||||
file_path = str_to_path(items[0].data(QtCore.Qt.UserRole))
|
||||
file_type = file_path.suffix.lower()[1:]
|
||||
if not self.display_type_combo_box.currentText():
|
||||
return False
|
||||
service_item.add_capability(ItemCapabilities.CanEditTitle)
|
||||
|
@ -292,29 +291,28 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
# force a nonexistent theme
|
||||
service_item.theme = -1
|
||||
for bitem in items:
|
||||
filename = presentation_file
|
||||
if filename is None:
|
||||
filename = bitem.data(QtCore.Qt.UserRole)
|
||||
(path, name) = os.path.split(filename)
|
||||
service_item.title = name
|
||||
if os.path.exists(filename):
|
||||
processor = self.find_controller_by_type(filename)
|
||||
if file_path is None:
|
||||
file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
|
||||
path, file_name = file_path.parent, file_path.name
|
||||
service_item.title = file_name
|
||||
if file_path.exists():
|
||||
processor = self.find_controller_by_type(file_path)
|
||||
if not processor:
|
||||
return False
|
||||
controller = self.controllers[processor]
|
||||
service_item.processor = None
|
||||
doc = controller.add_document(filename)
|
||||
if doc.get_thumbnail_path(1, True) is None or not os.path.isfile(
|
||||
os.path.join(doc.get_temp_folder(), 'mainslide001.png')):
|
||||
doc = controller.add_document(file_path)
|
||||
if doc.get_thumbnail_path(1, True) is None or \
|
||||
not (doc.get_temp_folder() / 'mainslide001.png').is_file():
|
||||
doc.load_presentation()
|
||||
i = 1
|
||||
image = os.path.join(doc.get_temp_folder(), 'mainslide{number:0>3d}.png'.format(number=i))
|
||||
thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i)
|
||||
while os.path.isfile(image):
|
||||
service_item.add_from_image(image, name, thumbnail=thumbnail)
|
||||
image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
|
||||
thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
|
||||
while image_path.is_file():
|
||||
service_item.add_from_image(str(image_path), file_name, thumbnail=str(thumbnail_path))
|
||||
i += 1
|
||||
image = os.path.join(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))
|
||||
image_path = doc.get_temp_folder() / 'mainslide{number:0>3d}.png'.format(number=i)
|
||||
thumbnail_path = doc.get_thumbnail_folder() / 'slide{number:d}.png'.format(number=i)
|
||||
service_item.add_capability(ItemCapabilities.HasThumbnails)
|
||||
doc.close_presentation()
|
||||
return True
|
||||
|
@ -324,34 +322,34 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'The presentation {name} no longer exists.'
|
||||
).format(name=filename))
|
||||
).format(name=file_path))
|
||||
return False
|
||||
else:
|
||||
service_item.processor = self.display_type_combo_box.currentText()
|
||||
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
||||
for bitem in items:
|
||||
filename = bitem.data(QtCore.Qt.UserRole)
|
||||
(path, name) = os.path.split(filename)
|
||||
service_item.title = name
|
||||
if os.path.exists(filename):
|
||||
file_path = str_to_path(bitem.data(QtCore.Qt.UserRole))
|
||||
path, file_name = file_path.parent, file_path.name
|
||||
service_item.title = file_name
|
||||
if file_path.exists():
|
||||
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:
|
||||
return False
|
||||
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:
|
||||
doc.load_presentation()
|
||||
i = 1
|
||||
img = doc.get_thumbnail_path(i, True)
|
||||
if img:
|
||||
thumbnail_path = doc.get_thumbnail_path(i, True)
|
||||
if thumbnail_path:
|
||||
# Get titles and notes
|
||||
titles, notes = doc.get_titles_and_notes()
|
||||
service_item.add_capability(ItemCapabilities.HasDisplayTitle)
|
||||
if notes.count('') != len(notes):
|
||||
service_item.add_capability(ItemCapabilities.HasNotes)
|
||||
service_item.add_capability(ItemCapabilities.HasThumbnails)
|
||||
while img:
|
||||
while thumbnail_path:
|
||||
# Use title and note if available
|
||||
title = ''
|
||||
if titles and len(titles) >= i:
|
||||
|
@ -359,9 +357,9 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
note = ''
|
||||
if notes and len(notes) >= i:
|
||||
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
|
||||
img = doc.get_thumbnail_path(i, True)
|
||||
thumbnail_path = doc.get_thumbnail_path(i, True)
|
||||
doc.close_presentation()
|
||||
return True
|
||||
else:
|
||||
|
@ -371,7 +369,7 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
'Missing Presentation'),
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'The presentation {name} is incomplete, '
|
||||
'please reload.').format(name=filename))
|
||||
'please reload.').format(name=file_path))
|
||||
return False
|
||||
else:
|
||||
# File is no longer present
|
||||
|
@ -379,18 +377,20 @@ class PresentationMediaItem(MediaManagerItem):
|
|||
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'Missing Presentation'),
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'The presentation {name} no longer exists.'
|
||||
).format(name=filename))
|
||||
).format(name=file_path))
|
||||
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
|
||||
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.
|
||||
|
||||
: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:
|
||||
return None
|
||||
for controller in self.controllers:
|
||||
|
|
|
@ -19,16 +19,15 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import copy
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
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.ui import HideMode
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -325,21 +324,25 @@ class MessageListener(object):
|
|||
is_live = message[1]
|
||||
item = message[0]
|
||||
hide_mode = message[2]
|
||||
file = item.get_frame_path()
|
||||
file_path = Path(item.get_frame_path())
|
||||
self.handler = item.processor
|
||||
# When starting presentation from the servicemanager we convert
|
||||
# PDF/XPS/OXPS-serviceitems into image-serviceitems. When started from the mediamanager
|
||||
# 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:
|
||||
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
|
||||
item_cpy = copy.copy(item)
|
||||
item.__init__(None)
|
||||
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:
|
||||
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
|
||||
item.footer = item_cpy.footer
|
||||
item.from_service = item_cpy.from_service
|
||||
|
@ -352,13 +355,13 @@ class MessageListener(object):
|
|||
self.handler = None
|
||||
else:
|
||||
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:
|
||||
return
|
||||
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:
|
||||
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:
|
||||
return
|
||||
if is_live:
|
||||
|
@ -370,7 +373,7 @@ class MessageListener(object):
|
|||
if self.handler is None:
|
||||
self.controller = controller
|
||||
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()
|
||||
|
||||
def slide(self, message):
|
||||
|
|
|
@ -23,12 +23,11 @@
|
|||
import os
|
||||
import logging
|
||||
import re
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError
|
||||
|
||||
from openlp.core.common import AppLocation, check_binary_exists
|
||||
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.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.
|
||||
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.
|
||||
:rtype: str | 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
|
||||
for line in runlog.splitlines():
|
||||
decoded_line = line.decode()
|
||||
|
@ -107,30 +107,29 @@ class PdfController(PresentationController):
|
|||
:return: True if program to open PDF-files was found, otherwise False.
|
||||
"""
|
||||
log.debug('check_installed Pdf')
|
||||
self.mudrawbin = ''
|
||||
self.mutoolbin = ''
|
||||
self.gsbin = ''
|
||||
self.mudrawbin = None
|
||||
self.mutoolbin = None
|
||||
self.gsbin = None
|
||||
self.also_supports = []
|
||||
# Use the user defined program if given
|
||||
if Settings().value('presentations/enable_pdf_program'):
|
||||
pdf_program = path_to_str(Settings().value('presentations/pdf_program'))
|
||||
program_type = self.process_check_binary(pdf_program)
|
||||
program_path = Settings().value('presentations/pdf_program')
|
||||
program_type = self.process_check_binary(program_path)
|
||||
if program_type == 'gs':
|
||||
self.gsbin = pdf_program
|
||||
self.gsbin = program_path
|
||||
elif program_type == 'mudraw':
|
||||
self.mudrawbin = pdf_program
|
||||
self.mudrawbin = program_path
|
||||
elif program_type == 'mutool':
|
||||
self.mutoolbin = pdf_program
|
||||
self.mutoolbin = program_path
|
||||
else:
|
||||
# Fallback to autodetection
|
||||
application_path = str(AppLocation.get_directory(AppLocation.AppDir))
|
||||
application_path = AppLocation.get_directory(AppLocation.AppDir)
|
||||
if is_win():
|
||||
# for windows we only accept mudraw.exe or mutool.exe in the base folder
|
||||
application_path = str(AppLocation.get_directory(AppLocation.AppDir))
|
||||
if os.path.isfile(os.path.join(application_path, 'mudraw.exe')):
|
||||
self.mudrawbin = os.path.join(application_path, 'mudraw.exe')
|
||||
elif os.path.isfile(os.path.join(application_path, 'mutool.exe')):
|
||||
self.mutoolbin = os.path.join(application_path, 'mutool.exe')
|
||||
if (application_path / 'mudraw.exe').is_file():
|
||||
self.mudrawbin = application_path / 'mudraw.exe'
|
||||
elif (application_path / 'mutool.exe').is_file():
|
||||
self.mutoolbin = application_path / 'mutool.exe'
|
||||
else:
|
||||
DEVNULL = open(os.devnull, 'wb')
|
||||
# First try to find mudraw
|
||||
|
@ -143,11 +142,11 @@ class PdfController(PresentationController):
|
|||
self.gsbin = which('gs')
|
||||
# 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:
|
||||
application_path = str(AppLocation.get_directory(AppLocation.AppDir))
|
||||
if os.path.isfile(os.path.join(application_path, 'mudraw')):
|
||||
self.mudrawbin = os.path.join(application_path, 'mudraw')
|
||||
elif os.path.isfile(os.path.join(application_path, 'mutool')):
|
||||
self.mutoolbin = os.path.join(application_path, 'mutool')
|
||||
application_path = AppLocation.get_directory(AppLocation.AppDir)
|
||||
if (application_path / 'mudraw').is_file():
|
||||
self.mudrawbin = application_path / 'mudraw'
|
||||
elif (application_path / 'mutool').is_file():
|
||||
self.mutoolbin = application_path / 'mutool'
|
||||
if self.mudrawbin or self.mutoolbin:
|
||||
self.also_supports = ['xps', 'oxps']
|
||||
return True
|
||||
|
@ -172,12 +171,15 @@ class PdfDocument(PresentationDocument):
|
|||
image-serviceitem on the fly and present as such. Therefore some of the 'playback'
|
||||
functions is not implemented.
|
||||
"""
|
||||
def __init__(self, controller, presentation):
|
||||
def __init__(self, controller, document_path):
|
||||
"""
|
||||
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')
|
||||
PresentationDocument.__init__(self, controller, presentation)
|
||||
super().__init__(controller, document_path)
|
||||
self.presentation = None
|
||||
self.blanked = False
|
||||
self.hidden = False
|
||||
|
@ -200,13 +202,13 @@ class PdfDocument(PresentationDocument):
|
|||
: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
|
||||
gs_resolution_script = str(AppLocation.get_directory(
|
||||
AppLocation.PluginsDir)) + '/presentations/lib/ghostscript_get_resolution.ps'
|
||||
gs_resolution_script = AppLocation.get_directory(
|
||||
AppLocation.PluginsDir) / 'presentations' / 'lib' / 'ghostscript_get_resolution.ps'
|
||||
# Run the script on the pdf to get the size
|
||||
runlog = []
|
||||
try:
|
||||
runlog = check_output([self.controller.gsbin, '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
|
||||
'-sFile=' + self.file_path, gs_resolution_script],
|
||||
runlog = check_output([str(self.controller.gsbin), '-dNOPAUSE', '-dNODISPLAY', '-dBATCH',
|
||||
'-sFile={file_path}'.format(file_path=self.file_path), str(gs_resolution_script)],
|
||||
startupinfo=self.startupinfo)
|
||||
except CalledProcessError as e:
|
||||
log.debug(' '.join(e.cmd))
|
||||
|
@ -240,46 +242,47 @@ class PdfDocument(PresentationDocument):
|
|||
:return: True is loading succeeded, otherwise False.
|
||||
"""
|
||||
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
|
||||
if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
|
||||
created_files = sorted(os.listdir(self.get_temp_folder()))
|
||||
for fn in created_files:
|
||||
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
|
||||
self.image_files.append(os.path.join(self.get_temp_folder(), fn))
|
||||
if (temp_dir_path / 'mainslide001.png').is_file():
|
||||
created_files = sorted(temp_dir_path.glob('*'))
|
||||
for image_path in created_files:
|
||||
if image_path.is_file():
|
||||
self.image_files.append(image_path)
|
||||
self.num_pages = len(self.image_files)
|
||||
return True
|
||||
size = ScreenList().current['size']
|
||||
# Generate images from PDF that will fit the frame.
|
||||
runlog = ''
|
||||
try:
|
||||
if not os.path.isdir(self.get_temp_folder()):
|
||||
os.makedirs(self.get_temp_folder())
|
||||
if not temp_dir_path.is_dir():
|
||||
temp_dir_path.mkdir(parents=True)
|
||||
# The %03d in the file name is handled by each binary
|
||||
if self.controller.mudrawbin:
|
||||
log.debug('loading presentation using mudraw')
|
||||
runlog = check_output([self.controller.mudrawbin, '-w', str(size.width()), '-h', str(size.height()),
|
||||
'-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path],
|
||||
runlog = check_output([str(self.controller.mudrawbin), '-w', str(size.width()),
|
||||
'-h', str(size.height()),
|
||||
'-o', str(temp_dir_path / 'mainslide%03d.png'), str(self.file_path)],
|
||||
startupinfo=self.startupinfo)
|
||||
elif self.controller.mutoolbin:
|
||||
log.debug('loading presentation using mutool')
|
||||
runlog = check_output([self.controller.mutoolbin, 'draw', '-w', str(size.width()), '-h',
|
||||
str(size.height()),
|
||||
'-o', os.path.join(self.get_temp_folder(), 'mainslide%03d.png'), self.file_path],
|
||||
runlog = check_output([str(self.controller.mutoolbin), 'draw', '-w', str(size.width()),
|
||||
'-h', str(size.height()), '-o', str(temp_dir_path / 'mainslide%03d.png'),
|
||||
str(self.file_path)],
|
||||
startupinfo=self.startupinfo)
|
||||
elif self.controller.gsbin:
|
||||
log.debug('loading presentation using gs')
|
||||
resolution = self.gs_get_resolution(size)
|
||||
runlog = check_output([self.controller.gsbin, '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
|
||||
'-r' + str(resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||
'-sOutputFile=' + os.path.join(self.get_temp_folder(), 'mainslide%03d.png'),
|
||||
self.file_path], startupinfo=self.startupinfo)
|
||||
created_files = sorted(os.listdir(self.get_temp_folder()))
|
||||
for fn in created_files:
|
||||
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
|
||||
self.image_files.append(os.path.join(self.get_temp_folder(), fn))
|
||||
runlog = check_output([str(self.controller.gsbin), '-dSAFER', '-dNOPAUSE', '-dBATCH', '-sDEVICE=png16m',
|
||||
'-r{res}'.format(res=resolution), '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||
'-sOutputFile={output}'.format(output=temp_dir_path / 'mainslide%03d.png'),
|
||||
str(self.file_path)], startupinfo=self.startupinfo)
|
||||
created_files = sorted(temp_dir_path.glob('*'))
|
||||
for image_path in created_files:
|
||||
if image_path.is_file():
|
||||
self.image_files.append(image_path)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
log.debug(runlog)
|
||||
log.exception(runlog)
|
||||
return False
|
||||
self.num_pages = len(self.image_files)
|
||||
# Create thumbnails
|
||||
|
|
|
@ -120,15 +120,16 @@ class PowerpointDocument(PresentationDocument):
|
|||
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.
|
||||
|
||||
: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')
|
||||
super(PowerpointDocument, self).__init__(controller, presentation)
|
||||
super().__init__(controller, document_path)
|
||||
self.presentation = None
|
||||
self.index_map = {}
|
||||
self.slide_count = 0
|
||||
|
@ -145,7 +146,7 @@ class PowerpointDocument(PresentationDocument):
|
|||
try:
|
||||
if not self.controller.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.create_thumbnails()
|
||||
self.create_titles_and_notes()
|
||||
|
@ -177,7 +178,7 @@ class PowerpointDocument(PresentationDocument):
|
|||
if not self.presentation.Slides(num + 1).SlideShowTransition.Hidden:
|
||||
self.index_map[key] = num + 1
|
||||
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
|
||||
self.slide_count = key - 1
|
||||
|
||||
|
@ -363,9 +364,8 @@ class PowerpointDocument(PresentationDocument):
|
|||
width=size.width(),
|
||||
horizontal=(right - left)))
|
||||
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 \
|
||||
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')
|
||||
self.presentation_hwnd = hwnd
|
||||
# Stop powerpoint from flashing in the taskbar
|
||||
|
|
|
@ -85,9 +85,9 @@ class PptviewController(PresentationController):
|
|||
if self.process:
|
||||
return
|
||||
log.debug('start PPTView')
|
||||
dll_path = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)),
|
||||
'plugins', 'presentations', 'lib', 'pptviewlib', 'pptviewlib.dll')
|
||||
self.process = cdll.LoadLibrary(dll_path)
|
||||
dll_path = AppLocation.get_directory(AppLocation.AppDir) \
|
||||
/ 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll'
|
||||
self.process = cdll.LoadLibrary(str(dll_path))
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
self.process.SetDebug(1)
|
||||
|
||||
|
@ -104,12 +104,15 @@ class PptviewDocument(PresentationDocument):
|
|||
"""
|
||||
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.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: File path to the document to load
|
||||
:rtype: None
|
||||
"""
|
||||
log.debug('Init Presentation PowerPoint')
|
||||
super(PptviewDocument, self).__init__(controller, presentation)
|
||||
super().__init__(controller, document_path)
|
||||
self.presentation = None
|
||||
self.ppt_id = None
|
||||
self.blanked = False
|
||||
|
@ -121,17 +124,16 @@ class PptviewDocument(PresentationDocument):
|
|||
the background PptView task started earlier.
|
||||
"""
|
||||
log.debug('LoadPresentation')
|
||||
temp_folder = self.get_temp_folder()
|
||||
temp_path = self.get_temp_folder()
|
||||
size = ScreenList().current['size']
|
||||
rect = RECT(size.x(), size.y(), size.right(), size.bottom())
|
||||
self.file_path = os.path.normpath(self.file_path)
|
||||
preview_path = os.path.join(temp_folder, 'slide')
|
||||
preview_path = temp_path / 'slide'
|
||||
# Ensure that the paths are null terminated
|
||||
byte_file_path = self.file_path.encode('utf-16-le') + b'\0'
|
||||
preview_path = preview_path.encode('utf-16-le') + b'\0'
|
||||
if not os.path.isdir(temp_folder):
|
||||
os.makedirs(temp_folder)
|
||||
self.ppt_id = self.controller.process.OpenPPT(byte_file_path, None, rect, preview_path)
|
||||
file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0'
|
||||
preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0'
|
||||
if not temp_path.is_dir():
|
||||
temp_path.mkdir(parents=True)
|
||||
self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16)
|
||||
if self.ppt_id >= 0:
|
||||
self.create_thumbnails()
|
||||
self.stop_presentation()
|
||||
|
@ -148,7 +150,7 @@ class PptviewDocument(PresentationDocument):
|
|||
return
|
||||
log.debug('create_thumbnails proceeding')
|
||||
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)
|
||||
|
||||
def create_titles_and_notes(self):
|
||||
|
@ -161,13 +163,12 @@ class PptviewDocument(PresentationDocument):
|
|||
"""
|
||||
titles = None
|
||||
notes = None
|
||||
filename = os.path.normpath(self.file_path)
|
||||
# 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",
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main"}
|
||||
# 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
|
||||
with zip_file.open('ppt/presentation.xml') as pres:
|
||||
tree = ElementTree.parse(pres)
|
||||
|
|
|
@ -19,15 +19,12 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -86,20 +83,27 @@ class PresentationDocument(object):
|
|||
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
|
||||
|
||||
:param controller:
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load.
|
||||
:rtype: None
|
||||
"""
|
||||
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.
|
||||
|
||||
:param openlp.core.common.path.Path document_path: Path to the document to load.
|
||||
:rtype: None
|
||||
"""
|
||||
self.slide_number = 0
|
||||
self.file_path = name
|
||||
check_directory_exists(Path(self.get_thumbnail_folder()))
|
||||
self.file_path = document_path
|
||||
check_directory_exists(self.get_thumbnail_folder())
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
|
@ -116,49 +120,54 @@ class PresentationDocument(object):
|
|||
a file, e.g. thumbnails
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self.get_thumbnail_folder()):
|
||||
shutil.rmtree(self.get_thumbnail_folder())
|
||||
if os.path.exists(self.get_temp_folder()):
|
||||
shutil.rmtree(self.get_temp_folder())
|
||||
thumbnail_folder_path = self.get_thumbnail_folder()
|
||||
temp_folder_path = self.get_temp_folder()
|
||||
if thumbnail_folder_path.exists():
|
||||
rmtree(thumbnail_folder_path)
|
||||
if temp_folder_path.exists():
|
||||
rmtree(temp_folder_path)
|
||||
except OSError:
|
||||
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):
|
||||
"""
|
||||
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
|
||||
if Settings().value('presentations/thumbnail_scheme') == 'md5':
|
||||
folder = md5_hash(self.file_path.encode('utf-8'))
|
||||
folder = md5_hash(bytes(self.file_path))
|
||||
else:
|
||||
folder = self.get_file_name()
|
||||
return os.path.join(self.controller.thumbnail_folder, folder)
|
||||
folder = self.file_path.name
|
||||
return Path(self.controller.thumbnail_folder, folder)
|
||||
|
||||
def get_temp_folder(self):
|
||||
"""
|
||||
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
|
||||
if Settings().value('presentations/thumbnail_scheme') == 'md5':
|
||||
folder = md5_hash(self.file_path.encode('utf-8'))
|
||||
folder = md5_hash(bytes(self.file_path))
|
||||
else:
|
||||
folder = folder = self.get_file_name()
|
||||
return os.path.join(self.controller.temp_folder, folder)
|
||||
folder = self.file_path.name
|
||||
return Path(self.controller.temp_folder, folder)
|
||||
|
||||
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)
|
||||
if not (last_image and os.path.isfile(last_image)):
|
||||
last_image_path = self.get_thumbnail_path(self.get_slide_count(), True)
|
||||
if not (last_image_path and last_image_path.is_file()):
|
||||
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):
|
||||
"""
|
||||
|
@ -241,25 +250,31 @@ class PresentationDocument(object):
|
|||
"""
|
||||
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.
|
||||
|
||||
: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():
|
||||
return
|
||||
if os.path.isfile(file):
|
||||
thumb_path = self.get_thumbnail_path(idx, False)
|
||||
create_thumb(file, thumb_path, False, QtCore.QSize(-1, 360))
|
||||
if image_path.is_file():
|
||||
thumb_path = self.get_thumbnail_path(index, False)
|
||||
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
|
||||
|
||||
:param slide_no: The slide an image is required for, starting at 1
|
||||
:param check_exists:
|
||||
:param int slide_no: The slide an image is required for, starting at 1
|
||||
: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')
|
||||
if os.path.isfile(path) or not check_exists:
|
||||
path = self.get_thumbnail_folder() / (self.controller.thumbnail_prefix + str(slide_no) + '.png')
|
||||
if path.is_file() or not check_exists:
|
||||
return path
|
||||
else:
|
||||
return None
|
||||
|
@ -302,44 +317,38 @@ class PresentationDocument(object):
|
|||
Reads the titles from the titles file and
|
||||
the notes files and returns the content in two lists
|
||||
"""
|
||||
titles = []
|
||||
notes = []
|
||||
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
|
||||
if os.path.exists(titles_file):
|
||||
try:
|
||||
with open(titles_file, encoding='utf-8') as fi:
|
||||
titles = fi.read().splitlines()
|
||||
except:
|
||||
log.exception('Failed to open/read existing titles file')
|
||||
titles = []
|
||||
titles_path = self.get_thumbnail_folder() / 'titles.txt'
|
||||
try:
|
||||
titles = titles_path.read_text().splitlines()
|
||||
except:
|
||||
log.exception('Failed to open/read existing titles file')
|
||||
titles = []
|
||||
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))
|
||||
note = ''
|
||||
if os.path.exists(notes_file):
|
||||
try:
|
||||
with open(notes_file, encoding='utf-8') as fn:
|
||||
note = fn.read()
|
||||
except:
|
||||
log.exception('Failed to open/read notes file')
|
||||
note = ''
|
||||
notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
|
||||
try:
|
||||
note = notes_path.read_text()
|
||||
except:
|
||||
log.exception('Failed to open/read notes file')
|
||||
note = ''
|
||||
notes.append(note)
|
||||
return titles, notes
|
||||
|
||||
def save_titles_and_notes(self, titles, notes):
|
||||
"""
|
||||
Performs the actual persisting of titles to the titles.txt
|
||||
and notes to the slideNote%.txt
|
||||
Performs the actual persisting of titles to the titles.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:
|
||||
titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt')
|
||||
with open(titles_file, mode='wt', encoding='utf-8') as fo:
|
||||
fo.writelines(titles)
|
||||
titles_path = self.get_thumbnail_folder() / 'titles.txt'
|
||||
titles_path.write_text('\n'.join(titles))
|
||||
if notes:
|
||||
for slide_no, note in enumerate(notes, 1):
|
||||
notes_file = os.path.join(self.get_thumbnail_folder(),
|
||||
'slideNotes{number:d}.txt'.format(number=slide_no))
|
||||
with open(notes_file, mode='wt', encoding='utf-8') as fn:
|
||||
fn.write(note)
|
||||
notes_path = self.get_thumbnail_folder() / 'slideNotes{number:d}.txt'.format(number=slide_no)
|
||||
notes_path.write_text(note)
|
||||
|
||||
|
||||
class PresentationController(object):
|
||||
|
@ -416,12 +425,11 @@ class PresentationController(object):
|
|||
self.document_class = document_class
|
||||
self.settings_section = self.plugin.settings_section
|
||||
self.available = None
|
||||
self.temp_folder = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), name)
|
||||
self.thumbnail_folder = os.path.join(
|
||||
str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
|
||||
self.temp_folder = AppLocation.get_section_data_path(self.settings_section) / name
|
||||
self.thumbnail_folder = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
|
||||
self.thumbnail_prefix = 'slide'
|
||||
check_directory_exists(Path(self.thumbnail_folder))
|
||||
check_directory_exists(Path(self.temp_folder))
|
||||
check_directory_exists(self.thumbnail_folder)
|
||||
check_directory_exists(self.temp_folder)
|
||||
|
||||
def enabled(self):
|
||||
"""
|
||||
|
@ -456,11 +464,15 @@ class PresentationController(object):
|
|||
log.debug('Kill')
|
||||
self.close_presentation()
|
||||
|
||||
def add_document(self, name):
|
||||
def add_document(self, document_path):
|
||||
"""
|
||||
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)
|
||||
return document
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ class PresentationTab(SettingsTab):
|
|||
"""
|
||||
Constructor
|
||||
"""
|
||||
self.parent = parent
|
||||
self.controllers = controllers
|
||||
super(PresentationTab, self).__init__(parent, title, visible_title, icon_path)
|
||||
self.activated = False
|
||||
|
@ -194,7 +193,7 @@ class PresentationTab(SettingsTab):
|
|||
pdf_program_path = self.program_path_edit.path
|
||||
enable_pdf_program = self.pdf_program_check_box.checkState()
|
||||
# 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
|
||||
if pdf_program_path != Settings().value(self.settings_section + '/pdf_program'):
|
||||
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):
|
||||
"""
|
||||
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 not PdfController.process_check_binary(new_path):
|
||||
critical_error_message_box(UiStrings().Error,
|
||||
|
|
|
@ -1,155 +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 #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import AppLocation, Registry, Settings, OpenLPMixin, UiStrings, check_directory_exists
|
||||
from openlp.core.lib import Plugin, StringContent, translate, build_icon
|
||||
from openlp.plugins.remotes.endpoint import remote_endpoint
|
||||
from openlp.plugins.remotes.deploy import download_and_check, download_sha256
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__default_settings__ = {
|
||||
'remotes/download version': '0000_00_00'
|
||||
}
|
||||
|
||||
|
||||
class RemotesPlugin(Plugin, OpenLPMixin):
|
||||
log.info('Remotes Plugin loaded')
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
remotes constructor
|
||||
"""
|
||||
super(RemotesPlugin, self).__init__('remotes', __default_settings__, {})
|
||||
self.icon_path = ':/plugins/plugin_remote.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.weight = -1
|
||||
register_endpoint(remote_endpoint)
|
||||
Registry().register_function('download_website', self.first_time)
|
||||
Registry().register_function('get_website_version', self.website_version)
|
||||
Registry().set_flag('website_version', '0001_01_01')
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Create the internal file structure if it does not exist
|
||||
:return:
|
||||
"""
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'assets')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'images')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'static', 'index')
|
||||
check_directory_exists(AppLocation.get_section_data_path('remotes') / 'templates')
|
||||
|
||||
@staticmethod
|
||||
def about():
|
||||
"""
|
||||
Information about this plugin
|
||||
"""
|
||||
about_text = translate(
|
||||
'RemotePlugin',
|
||||
'<strong>Web Interface</strong>'
|
||||
'<br />The web interface plugin provides the ability to develop web based interfaces using OpenLP web '
|
||||
'services.\nPredefined interfaces can be download as well as custom developed interfaces.')
|
||||
return about_text
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
"""
|
||||
Called to define all translatable texts of the plugin
|
||||
"""
|
||||
# Name PluginList
|
||||
self.text_strings[StringContent.Name] = {
|
||||
'singular': translate('RemotePlugin', 'Web Interface', 'name singular'),
|
||||
'plural': translate('RemotePlugin', 'Web Interface', 'name plural')
|
||||
}
|
||||
# Name for MediaDockManager, SettingsManager
|
||||
self.text_strings[StringContent.VisibleName] = {
|
||||
'title': translate('RemotePlugin', 'Web Remote', 'container title')
|
||||
}
|
||||
|
||||
def first_time(self):
|
||||
"""
|
||||
Import web site code if active
|
||||
"""
|
||||
self.application.process_events()
|
||||
progress = Progress(self)
|
||||
progress.forceShow()
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
download_and_check(progress)
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
progress.close()
|
||||
self.application.process_events()
|
||||
Settings().setValue('remotes/download version', self.version)
|
||||
|
||||
def website_version(self):
|
||||
"""
|
||||
Download and save the website version and sha256
|
||||
:return: None
|
||||
"""
|
||||
sha256, self.version = download_sha256()
|
||||
Registry().set_flag('website_sha256', sha256)
|
||||
Registry().set_flag('website_version', self.version)
|
||||
|
||||
|
||||
class Progress(QtWidgets.QProgressDialog):
|
||||
"""
|
||||
Local class to handle download display based and supporting httputils:get_web_page
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(Progress, self).__init__(parent.main_window)
|
||||
self.parent = parent
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowTitle(translate('RemotePlugin', 'Importing Website'))
|
||||
self.setLabelText(UiStrings().StartingImport)
|
||||
self.setCancelButton(None)
|
||||
self.setRange(0, 1)
|
||||
self.setMinimumDuration(0)
|
||||
self.was_cancelled = False
|
||||
self.previous_size = 0
|
||||
|
||||
def _download_progress(self, count, block_size):
|
||||
"""
|
||||
Calculate and display the download progress.
|
||||
"""
|
||||
increment = (count * block_size) - self.previous_size
|
||||
self._increment_progress_bar(None, increment)
|
||||
self.previous_size = count * block_size
|
||||
|
||||
def _increment_progress_bar(self, status_text, increment=1):
|
||||
"""
|
||||
Update the wizard progress page.
|
||||
|
||||
:param status_text: Current status information to display.
|
||||
:param increment: The value to increment the progress bar by.
|
||||
"""
|
||||
if status_text:
|
||||
self.setText(status_text)
|
||||
if increment > 0:
|
||||
self.setValue(self.value() + increment)
|
||||
self.parent.application.process_events()
|
|
@ -62,7 +62,7 @@ def songs_service(request):
|
|||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'songs', log)
|
||||
return service(request, 'songs', log)
|
||||
|
||||
|
||||
@api_songs_endpoint.route('songs/search')
|
||||
|
@ -95,6 +95,6 @@ def songs_service_api(request):
|
|||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'songs', log)
|
||||
return service(request, 'songs', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
||||
|
|
|
@ -62,7 +62,7 @@ import re
|
|||
from lxml import etree, objectify
|
||||
|
||||
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.plugins.songs.lib import VerseType, clean_song
|
||||
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.
|
||||
song_xml.set('xmlns', NAMESPACE)
|
||||
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('modifiedIn', application_name)
|
||||
# "Convert" 2012-08-27 11:49:15 to 2012-08-27T11:49:15.
|
||||
|
|
|
@ -25,10 +25,10 @@ The :mod:`db` module provides the ability to provide a csv file of all songs
|
|||
import csv
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
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.ui.lib.filedialog import FileDialog
|
||||
from openlp.plugins.songs.lib.db import Song
|
||||
|
||||
|
||||
|
@ -42,58 +42,55 @@ def report_song_list():
|
|||
"""
|
||||
main_window = Registry().get('main_window')
|
||||
plugin = Registry().get('songs').plugin
|
||||
report_file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
|
||||
report_file_path, filter_used = FileDialog.getSaveFileName(
|
||||
main_window,
|
||||
translate('SongPlugin.ReportSongList', 'Save File'),
|
||||
translate('SongPlugin.ReportSongList', 'song_extract.csv'),
|
||||
Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')),
|
||||
translate('SongPlugin.ReportSongList', 'CSV format (*.csv)'))
|
||||
|
||||
if not report_file_name:
|
||||
if report_file_path is None:
|
||||
main_window.error_message(
|
||||
translate('SongPlugin.ReportSongList', 'Output Path Not Selected'),
|
||||
translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your '
|
||||
'report. \nPlease select an existing path '
|
||||
'on your computer.')
|
||||
translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n'
|
||||
'Please select an existing path on your computer.')
|
||||
)
|
||||
return
|
||||
if not report_file_name.endswith('csv'):
|
||||
report_file_name += '.csv'
|
||||
file_handle = None
|
||||
report_file_path.with_suffix('.csv')
|
||||
Registry().get('application').set_busy_cursor()
|
||||
try:
|
||||
file_handle = open(report_file_name, 'wt')
|
||||
fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
|
||||
writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
|
||||
headers = dict((n, n) for n in fieldnames)
|
||||
writer.writerow(headers)
|
||||
song_list = plugin.manager.get_all_objects(Song)
|
||||
for song in song_list:
|
||||
author_list = []
|
||||
for author_song in song.authors_songs:
|
||||
author_list.append(author_song.author.display_name)
|
||||
author_string = ' | '.join(author_list)
|
||||
book_list = []
|
||||
for book_song in song.songbook_entries:
|
||||
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_string = ' | '.join(book_list)
|
||||
topic_list = []
|
||||
for topic_song in song.topics:
|
||||
if hasattr(topic_song, 'name'):
|
||||
topic_list.append(topic_song.name)
|
||||
topic_string = ' | '.join(topic_list)
|
||||
writer.writerow({'Title': song.title,
|
||||
'Alternative Title': song.alternate_title,
|
||||
'Copyright': song.copyright,
|
||||
'Author(s)': author_string,
|
||||
'Song Book': book_string,
|
||||
'Topic': topic_string})
|
||||
Registry().get('application').set_normal_cursor()
|
||||
main_window.information_message(
|
||||
translate('SongPlugin.ReportSongList', 'Report Creation'),
|
||||
translate('SongPlugin.ReportSongList',
|
||||
'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
|
||||
)
|
||||
with report_file_path.open('wt') as file_handle:
|
||||
fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic')
|
||||
writer = csv.DictWriter(file_handle, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
|
||||
headers = dict((n, n) for n in fieldnames)
|
||||
writer.writerow(headers)
|
||||
song_list = plugin.manager.get_all_objects(Song)
|
||||
for song in song_list:
|
||||
author_list = []
|
||||
for author_song in song.authors_songs:
|
||||
author_list.append(author_song.author.display_name)
|
||||
author_string = ' | '.join(author_list)
|
||||
book_list = []
|
||||
for book_song in song.songbook_entries:
|
||||
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_string = ' | '.join(book_list)
|
||||
topic_list = []
|
||||
for topic_song in song.topics:
|
||||
if hasattr(topic_song, 'name'):
|
||||
topic_list.append(topic_song.name)
|
||||
topic_string = ' | '.join(topic_list)
|
||||
writer.writerow({'Title': song.title,
|
||||
'Alternative Title': song.alternate_title,
|
||||
'Copyright': song.copyright,
|
||||
'Author(s)': author_string,
|
||||
'Song Book': book_string,
|
||||
'Topic': topic_string})
|
||||
Registry().get('application').set_normal_cursor()
|
||||
main_window.information_message(
|
||||
translate('SongPlugin.ReportSongList', 'Report Creation'),
|
||||
translate('SongPlugin.ReportSongList',
|
||||
'Report \n{name} \nhas been successfully created. ').format(name=report_file_path)
|
||||
)
|
||||
except OSError as ose:
|
||||
Registry().get('application').set_normal_cursor()
|
||||
log.exception('Failed to write out song usage records')
|
||||
|
@ -101,6 +98,3 @@ def report_song_list():
|
|||
translate('SongPlugin.ReportSongList',
|
||||
'An error occurred while extracting: {error}'
|
||||
).format(error=ose.strerror))
|
||||
finally:
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import translate
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
@ -60,7 +59,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
|
|||
|
||||
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.
|
||||
: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
|
||||
"""
|
||||
log.debug('accept')
|
||||
path = path_to_str(self.report_path_edit.path)
|
||||
path = self.report_path_edit.path
|
||||
if not path:
|
||||
self.main_window.error_message(
|
||||
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.')
|
||||
)
|
||||
return
|
||||
check_directory_exists(Path(path))
|
||||
check_directory_exists(path)
|
||||
file_name = translate('SongUsagePlugin.SongUsageDetailForm',
|
||||
'usage_detail_{old}_{new}.txt'
|
||||
).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.usagedate < self.to_date_calendar.selectedDate().toPyDate()),
|
||||
[SongUsageItem.usagedate, SongUsageItem.usagetime])
|
||||
report_file_name = os.path.join(path, file_name)
|
||||
file_handle = None
|
||||
report_file_name = path / file_name
|
||||
try:
|
||||
file_handle = open(report_file_name, 'wb')
|
||||
for instance in usage:
|
||||
record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",'
|
||||
'\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime,
|
||||
title=instance.title, copyright=instance.copyright,
|
||||
ccli=instance.ccl_number, authors=instance.authors,
|
||||
name=instance.plugin_name, source=instance.source)
|
||||
file_handle.write(record.encode('utf-8'))
|
||||
self.main_window.information_message(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
|
||||
translate('SongUsagePlugin.SongUsageDetailForm',
|
||||
'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
|
||||
)
|
||||
with report_file_name.open('wb') as file_handle:
|
||||
for instance in usage:
|
||||
record = ('\"{date}\",\"{time}\",\"{title}\",\"{copyright}\",\"{ccli}\",\"{authors}\",'
|
||||
'\"{name}\",\"{source}\"\n').format(date=instance.usagedate, time=instance.usagetime,
|
||||
title=instance.title, copyright=instance.copyright,
|
||||
ccli=instance.ccl_number, authors=instance.authors,
|
||||
name=instance.plugin_name, source=instance.source)
|
||||
file_handle.write(record.encode('utf-8'))
|
||||
self.main_window.information_message(
|
||||
translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation'),
|
||||
translate('SongUsagePlugin.SongUsageDetailForm',
|
||||
'Report \n{name} \nhas been successfully created. ').format(name=report_file_name)
|
||||
)
|
||||
except OSError as ose:
|
||||
log.exception('Failed to write out song usage records')
|
||||
critical_error_message_box(translate('SongUsagePlugin.SongUsageDetailForm', 'Report Creation Failed'),
|
||||
translate('SongUsagePlugin.SongUsageDetailForm',
|
||||
'An error occurred while creating the report: {error}'
|
||||
).format(error=ose.strerror))
|
||||
finally:
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
self.close()
|
||||
|
|
|
@ -12,7 +12,7 @@ environment:
|
|||
|
||||
install:
|
||||
# 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
|
||||
- "%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/)
|
||||
|
|
|
@ -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
|
||||
modules, simply run this script::
|
||||
|
||||
@:~$ ./check_dependencies.py
|
||||
$ ./check_dependencies.py
|
||||
|
||||
"""
|
||||
import os
|
||||
|
@ -45,7 +45,7 @@ IS_MAC = sys.platform.startswith('dar')
|
|||
|
||||
|
||||
VERS = {
|
||||
'Python': '3.0',
|
||||
'Python': '3.4',
|
||||
'PyQt5': '5.0',
|
||||
'Qt5': '5.0',
|
||||
'sqlalchemy': '0.5',
|
||||
|
@ -97,7 +97,8 @@ MODULES = [
|
|||
'asyncio',
|
||||
'waitress',
|
||||
'six',
|
||||
'webob'
|
||||
'webob',
|
||||
'requests'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -22,14 +22,12 @@
|
|||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.plugins.remotes.deploy import deploy_zipfile
|
||||
from openlp.core.api.deploy import deploy_zipfile
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
class TestRemoteDeploy(TestCase):
|
||||
|
@ -54,6 +52,7 @@ class TestRemoteDeploy(TestCase):
|
|||
Remote Deploy tests - test the dummy zip file is processed correctly
|
||||
"""
|
||||
# GIVEN: A new downloaded zip file
|
||||
aa = TEST_PATH
|
||||
zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip')
|
||||
app_root = os.path.join(self.app_root, 'site.zip')
|
||||
shutil.copyfile(zip_file, app_root)
|
|
@ -70,7 +70,7 @@ class TestWSServer(TestCase, TestMixin):
|
|||
"""
|
||||
# GIVEN: A new httpserver
|
||||
# WHEN: I start the server
|
||||
server = WebSocketServer()
|
||||
WebSocketServer()
|
||||
|
||||
# THEN: the api environment should have been created
|
||||
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
|
||||
"""
|
||||
# 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.service_id = 21
|
||||
mocked_live_controller = MagicMock()
|
||||
|
@ -105,8 +105,15 @@ class TestWSServer(TestCase, TestMixin):
|
|||
mocked_live_controller.desktop_screen.isChecked.return_value = False
|
||||
Registry().register('live_controller', mocked_live_controller)
|
||||
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
|
||||
poll_json = self.poll.poll()
|
||||
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']['display'], 'The display return value should be False')
|
||||
|
|
|
@ -24,11 +24,11 @@ Functional tests to test the AppLocation class and related methods.
|
|||
"""
|
||||
import os
|
||||
import tempfile
|
||||
import socket
|
||||
from unittest import TestCase
|
||||
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
|
||||
|
||||
|
@ -67,7 +67,7 @@ class TestHttpUtils(TestCase, TestMixin):
|
|||
"""
|
||||
with patch('openlp.core.common.httputils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
# GIVEN: The system is Windows
|
||||
mocked_sys.platform = 'win32'
|
||||
|
||||
# WHEN: We call get_user_agent()
|
||||
|
@ -82,7 +82,7 @@ class TestHttpUtils(TestCase, TestMixin):
|
|||
"""
|
||||
with patch('openlp.core.common.httputils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
# GIVEN: The system is macOS
|
||||
mocked_sys.platform = 'darwin'
|
||||
|
||||
# WHEN: We call get_user_agent()
|
||||
|
@ -97,7 +97,7 @@ class TestHttpUtils(TestCase, TestMixin):
|
|||
"""
|
||||
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'
|
||||
|
||||
# WHEN: We call get_user_agent()
|
||||
|
@ -119,182 +119,125 @@ class TestHttpUtils(TestCase, TestMixin):
|
|||
# THEN: None should be returned
|
||||
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
|
||||
"""
|
||||
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \
|
||||
patch('openlp.core.common.Registry') as MockRegistry:
|
||||
# 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'
|
||||
# GIVEN: Mocked out objects and a fake URL
|
||||
mocked_requests.get.return_value = MagicMock(text='text')
|
||||
mocked_get_user_agent.return_value = 'user_agent'
|
||||
fake_url = 'this://is.a.fake/url'
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url)
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url)
|
||||
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
MockRequest.assert_called_with(fake_url)
|
||||
mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent')
|
||||
self.assertEqual(1, mocked_request_object.add_header.call_count,
|
||||
'There should only be 1 call to add_header')
|
||||
mock_get_user_agent.assert_called_with()
|
||||
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')
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'},
|
||||
proxies=None, timeout=30.0)
|
||||
mocked_get_user_agent.assert_called_once_with()
|
||||
assert MockRegistry.call_count == 0, 'The Registry() object should have never been called'
|
||||
assert returned_page == 'text', '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
|
||||
"""
|
||||
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent:
|
||||
# GIVEN: Mocked out objects, a fake URL and a fake header
|
||||
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'
|
||||
fake_header = ('Fake-Header', 'fake value')
|
||||
# GIVEN: Mocked out objects, a fake URL and a fake header
|
||||
mocked_requests.get.return_value = MagicMock(text='text')
|
||||
mocked_get_user_agent.return_value = 'user_agent'
|
||||
fake_url = 'this://is.a.fake/url'
|
||||
fake_headers = {'Fake-Header': 'fake value'}
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=fake_header)
|
||||
# WHEN: The get_web_page() method is called
|
||||
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
|
||||
MockRequest.assert_called_with(fake_url)
|
||||
mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1])
|
||||
self.assertEqual(2, mocked_request_object.add_header.call_count,
|
||||
'There should only be 2 calls to add_header')
|
||||
mock_get_user_agent.assert_called_with()
|
||||
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')
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
expected_headers = dict(fake_headers)
|
||||
expected_headers.update({'User-Agent': 'user_agent'})
|
||||
mocked_requests.get.assert_called_once_with(fake_url, headers=expected_headers,
|
||||
proxies=None, timeout=30.0)
|
||||
mocked_get_user_agent.assert_called_with()
|
||||
assert returned_page == 'text', '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
|
||||
"""
|
||||
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent:
|
||||
# GIVEN: Mocked out objects, a fake URL and a fake header
|
||||
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')
|
||||
# GIVEN: Mocked out objects, a fake URL and a fake header
|
||||
mocked_requests.get.return_value = MagicMock(text='text')
|
||||
fake_url = 'this://is.a.fake/url'
|
||||
user_agent_headers = {'User-Agent': 'OpenLP/2.2.0'}
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=user_agent_header)
|
||||
# WHEN: The get_web_page() method is called
|
||||
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
|
||||
MockRequest.assert_called_with(fake_url)
|
||||
mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1])
|
||||
self.assertEqual(1, mocked_request_object.add_header.call_count,
|
||||
'There should only be 1 call to add_header')
|
||||
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')
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
mocked_requests.get.assert_called_once_with(fake_url, headers=user_agent_headers,
|
||||
proxies=None, timeout=30.0)
|
||||
assert mocked_get_user_agent.call_count == 0, 'get_user_agent() should not have been called'
|
||||
assert returned_page == 'text', 'The returned page should be "test"'
|
||||
|
||||
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()
|
||||
"""
|
||||
with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \
|
||||
patch('openlp.core.common.httputils.Registry') as MockRegistry:
|
||||
# GIVEN: Mocked out objects, 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'
|
||||
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'
|
||||
# GIVEN: Mocked out objects, a fake URL
|
||||
mocked_requests.get.return_value = MagicMock(text='text')
|
||||
mocked_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
|
||||
returned_page = get_web_page(fake_url, update_openlp=True)
|
||||
# WHEN: The get_web_page() method is called
|
||||
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
|
||||
MockRequest.assert_called_with(fake_url)
|
||||
mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent')
|
||||
self.assertEqual(1, mocked_request_object.add_header.call_count,
|
||||
'There should only be 1 call to add_header')
|
||||
mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
|
||||
mocked_page_object.geturl.assert_called_with()
|
||||
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')
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
mocked_requests.get.assert_called_once_with(fake_url, headers={'User-Agent': 'user_agent'},
|
||||
proxies=None, timeout=30.0)
|
||||
mocked_get_user_agent.assert_called_once_with()
|
||||
mocked_registry_object.get.assert_called_with('application')
|
||||
mocked_application_object.process_events.assert_called_with()
|
||||
assert returned_page == 'text', '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, \
|
||||
patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent:
|
||||
# GIVEN: Mocked out objects, 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'
|
||||
# GIVEN: Mocked out objects, a fake URL
|
||||
mocked_requests.head.return_value = MagicMock(headers={'Content-Length': 100})
|
||||
fake_url = 'this://is.a.fake/url'
|
||||
|
||||
# WHEN: The get_url_file_size() method is called
|
||||
size = get_url_file_size(fake_url)
|
||||
# WHEN: The get_url_file_size() method is called
|
||||
file_size = get_url_file_size(fake_url)
|
||||
|
||||
# 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)
|
||||
# THEN: The correct methods are called with the correct arguments and a web page is returned
|
||||
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')
|
||||
def test_socket_timeout(self, mocked_urlopen):
|
||||
@patch('openlp.core.common.httputils.requests')
|
||||
def test_socket_timeout(self, mocked_requests):
|
||||
"""
|
||||
Test socket timeout gets caught
|
||||
"""
|
||||
# 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
|
||||
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
|
||||
# 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')
|
||||
|
||||
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')
|
||||
assert not os.path.exists(self.tempfile), 'tempfile should have been deleted'
|
||||
|
|
|
@ -24,8 +24,209 @@ Package to test the openlp.core.common.path package.
|
|||
"""
|
||||
import os
|
||||
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):
|
||||
|
|
|
@ -1,64 +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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.common.versionchecker package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
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 setUp(self):
|
||||
"""
|
||||
Create an instance and a few example actions.
|
||||
"""
|
||||
self.build_settings()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up
|
||||
"""
|
||||
self.destroy_settings()
|
||||
|
||||
def test_version_thread_triggered(self):
|
||||
"""
|
||||
Test the version thread call does not trigger UI
|
||||
:return:
|
||||
"""
|
||||
# GIVEN: a equal version setup and the data is not today.
|
||||
mocked_main_window = MagicMock()
|
||||
Settings().setValue('core/last version test', '1950-04-01')
|
||||
# WHEN: We check to see if the version is different .
|
||||
with patch('PyQt5.QtCore.QThread'),\
|
||||
patch('openlp.core.common.versionchecker.get_application_version') as mocked_get_application_version:
|
||||
mocked_get_application_version.return_value = {'version': '1.0.0', 'build': '', 'full': '2.0.4'}
|
||||
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')
|
|
@ -32,7 +32,7 @@ from PyQt5 import QtCore, QtGui
|
|||
from openlp.core.common.path import Path
|
||||
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, \
|
||||
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'))
|
||||
|
||||
|
@ -595,93 +595,46 @@ class TestLib(TestCase):
|
|||
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
|
||||
with patch('openlp.core.lib.os') as mocked_os:
|
||||
file_path = 'path/to/file'
|
||||
thumb_path = 'path/to/thumb'
|
||||
mocked_os.path.exists.return_value = False
|
||||
with patch.object(Path, 'exists', return_value=False) as mocked_path_exists:
|
||||
file_path = Path('path', 'to', 'file')
|
||||
thumb_path = Path('path', 'to', 'thumb')
|
||||
|
||||
# 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 False
|
||||
mocked_os.path.exists.assert_called_with(thumb_path)
|
||||
assert result is False, 'The result should be False'
|
||||
thumb_path.exists.assert_called_once_with()
|
||||
self.assertFalse(result, 'The result should be False')
|
||||
|
||||
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
|
||||
"""
|
||||
# GIVEN: A mocked out os module, functions rigged to work for us, and fake paths to a file and a thumb
|
||||
with patch('openlp.core.lib.os') as mocked_os:
|
||||
file_path = 'path/to/file'
|
||||
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 = [file_mocked_stat, thumb_mocked_stat]
|
||||
with patch.object(Path, 'exists'), patch.object(Path, 'stat'):
|
||||
# GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
|
||||
file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
|
||||
thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)})
|
||||
|
||||
# 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
|
||||
# mocked_os.path.exists.assert_called_with(thumb_path)
|
||||
# THEN: `validate_thumb` should return True
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_validate_thumb_file_exists_and_older(self):
|
||||
"""
|
||||
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
|
||||
with patch('openlp.core.lib.os') as mocked_os:
|
||||
file_path = 'path/to/file'
|
||||
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
|
||||
# GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
|
||||
file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
|
||||
thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)})
|
||||
|
||||
# WHEN: we run the validate_thumb() function
|
||||
result = validate_thumb(file_path, thumb_path)
|
||||
# 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 False
|
||||
mocked_os.path.exists.assert_called_with(thumb_path)
|
||||
mocked_os.stat.assert_any_call(file_path)
|
||||
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})
|
||||
# THEN: `validate_thumb` should return False
|
||||
thumb_path.stat.assert_called_once_with()
|
||||
self.assertFalse(result, 'The result should be False')
|
||||
|
||||
def test_resize_thumb(self):
|
||||
"""
|
||||
|
|
|
@ -22,8 +22,9 @@
|
|||
"""
|
||||
Package to test the openlp.core.lib.theme package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.lib.theme import Theme
|
||||
|
||||
|
@ -79,16 +80,16 @@ class TestTheme(TestCase):
|
|||
"""
|
||||
# GIVEN: A theme object
|
||||
theme = Theme()
|
||||
theme.theme_name = 'MyBeautifulTheme '
|
||||
theme.background_filename = ' video.mp4'
|
||||
theme.theme_name = 'MyBeautifulTheme'
|
||||
theme.background_filename = Path('video.mp4')
|
||||
theme.background_type = 'video'
|
||||
path = os.path.expanduser('~')
|
||||
path = Path.home()
|
||||
|
||||
# WHEN: Theme.extend_image_filename is run
|
||||
theme.extend_image_filename(path)
|
||||
|
||||
# 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('MyBeautifulTheme', theme.theme_name)
|
||||
|
||||
|
|
|
@ -47,13 +47,13 @@ class TestFirstTimeForm(TestCase, TestMixin):
|
|||
# THEN: A web browser is opened
|
||||
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute')
|
||||
|
||||
@patch('openlp.core.ui.aboutform.get_application_version')
|
||||
def test_about_form_build_number(self, mocked_get_application_version):
|
||||
@patch('openlp.core.ui.aboutform.get_version')
|
||||
def test_about_form_build_number(self, mocked_get_version):
|
||||
"""
|
||||
Test that the build number is added to the about form
|
||||
"""
|
||||
# GIVEN: A mocked out get_application_version function
|
||||
mocked_get_application_version.return_value = {'version': '3.1.5', 'build': '3000'}
|
||||
# GIVEN: A mocked out get_version function
|
||||
mocked_get_version.return_value = {'version': '3.1.5', 'build': '3000'}
|
||||
|
||||
# WHEN: The about form is created
|
||||
about_form = AboutForm(None)
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
"""
|
||||
Package to test the openlp.core.ui.exeptionform package.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
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.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.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.bs4")
|
||||
@patch("openlp.core.ui.exceptionform.etree")
|
||||
|
@ -64,18 +64,10 @@ class TestExceptionForm(TestMixin, TestCase):
|
|||
"""
|
||||
Test functionality of exception form functions
|
||||
"""
|
||||
def __method_template_for_class_patches(self,
|
||||
__PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
|
||||
mocked_python_version,
|
||||
mocked_platform,
|
||||
mocked_is_linux,
|
||||
mocked_etree,
|
||||
mocked_bs4,
|
||||
mocked_sqlalchemy,
|
||||
mocked_application_version,
|
||||
mocked_openlurl,
|
||||
mocked_qversion,
|
||||
):
|
||||
def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
|
||||
mocked_python_version, mocked_platform, mocked_is_linux,
|
||||
mocked_etree, mocked_bs4, mocked_sqlalchemy, mocked_get_version,
|
||||
mocked_openlurl, mocked_qversion):
|
||||
"""
|
||||
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_qversion.return_value = 'Qt5 test'
|
||||
mocked_is_linux.return_value = False
|
||||
mocked_application_version.return_value = 'Trunk Test'
|
||||
mocked_get_version.return_value = 'Trunk Test'
|
||||
|
||||
def setUp(self):
|
||||
self.setup_application()
|
||||
|
@ -103,26 +95,14 @@ class TestExceptionForm(TestMixin, TestCase):
|
|||
os.remove(self.tempfile)
|
||||
|
||||
@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.QUrlQuery.addQueryItem")
|
||||
@patch("openlp.core.ui.exceptionform.Qt")
|
||||
def test_on_send_report_button_clicked(self,
|
||||
mocked_qt,
|
||||
mocked_add_query_item,
|
||||
mocked_qurl,
|
||||
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,
|
||||
):
|
||||
def test_on_send_report_button_clicked(self, mocked_qt, mocked_add_query_item, mocked_qurl, mocked_file_dialog,
|
||||
mocked_ui_exception_dialog, mocked_python_version, mocked_platform,
|
||||
mocked_is_linux, mocked_etree, mocked_bs4, mocked_sqlalchemy,
|
||||
mocked_get_version, mocked_openlurl, mocked_qversion):
|
||||
"""
|
||||
Test send report creates the proper system information text
|
||||
"""
|
||||
|
@ -134,42 +114,33 @@ class TestExceptionForm(TestMixin, TestCase):
|
|||
mocked_platform.return_value = 'Nose Test'
|
||||
mocked_qversion.return_value = 'Qt5 test'
|
||||
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_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.file_attachment = None
|
||||
|
||||
with patch.object(test_form, '_pyuno_import') as mock_pyuno:
|
||||
with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
|
||||
with 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'
|
||||
with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
|
||||
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_send_report_button_clicked()
|
||||
# WHEN: on_save_report_button_clicked called
|
||||
test_form.on_send_report_button_clicked()
|
||||
|
||||
# THEN: Verify strings were formatted properly
|
||||
mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
|
||||
|
||||
@patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName")
|
||||
@patch("openlp.core.ui.exceptionform.Qt")
|
||||
def test_on_save_report_button_clicked(self,
|
||||
mocked_qt,
|
||||
mocked_save_filename,
|
||||
mocked_python_version,
|
||||
mocked_platform,
|
||||
mocked_is_linux,
|
||||
mocked_etree,
|
||||
mocked_bs4,
|
||||
mocked_sqlalchemy,
|
||||
mocked_application_version,
|
||||
mocked_openlurl,
|
||||
mocked_qversion,
|
||||
):
|
||||
def test_on_save_report_button_clicked(self, mocked_qt, mocked_save_filename, mocked_python_version,
|
||||
mocked_platform, mocked_is_linux, mocked_etree, mocked_bs4,
|
||||
mocked_sqlalchemy, mocked_get_version, mocked_openlurl,
|
||||
mocked_qversion):
|
||||
"""
|
||||
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_qt.PYQT_VERSION_STR = 'PyQt5 Test'
|
||||
mocked_is_linux.return_value = False
|
||||
mocked_application_version.return_value = 'Trunk Test'
|
||||
mocked_save_filename.return_value = (Path('testfile.txt'), 'filter')
|
||||
mocked_get_version.return_value = 'Trunk Test'
|
||||
|
||||
test_form = exceptionform.ExceptionForm()
|
||||
test_form.file_attachment = None
|
||||
with patch.object(Path, 'open') as mocked_path_open:
|
||||
test_path = Path('testfile.txt')
|
||||
mocked_save_filename.return_value = test_path, 'ext'
|
||||
|
||||
with patch.object(test_form, '_pyuno_import') as mock_pyuno:
|
||||
with patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback:
|
||||
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'
|
||||
test_form = exceptionform.ExceptionForm()
|
||||
test_form.file_attachment = None
|
||||
|
||||
# WHEN: on_save_report_button_clicked called
|
||||
test_form.on_save_report_button_clicked()
|
||||
with patch.object(test_form, '_pyuno_import') as mock_pyuno, \
|
||||
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
|
||||
# self.maxDiff = None
|
||||
check_text = "call().write({text})".format(text=MAIL_ITEM_TEXT.__repr__())
|
||||
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")
|
||||
mocked_path_open.assert_has_calls([call().__enter__().write(MAIL_ITEM_TEXT)])
|
||||
|
|
|
@ -22,9 +22,6 @@
|
|||
"""
|
||||
Package to test the openlp.core.utils.__init__ package.
|
||||
"""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
|
@ -37,20 +34,21 @@ class TestFirstTimeWizard(TestMixin, TestCase):
|
|||
"""
|
||||
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
|
||||
"""
|
||||
# GIVEN: Initial settings and mocks
|
||||
with patch.object(urllib.request, 'urlopen') as mocked_urlopen:
|
||||
mocked_urlopen.side_effect = ConnectionError
|
||||
mocked_requests.get.side_effect = IOError('Unable to connect')
|
||||
|
||||
# WHEN: A webpage is requested
|
||||
try:
|
||||
get_web_page(url='http://localhost')
|
||||
except:
|
||||
pass
|
||||
# WHEN: A webpage is requested
|
||||
try:
|
||||
get_web_page('http://localhost')
|
||||
except Exception as e:
|
||||
assert isinstance(e, ConnectionError)
|
||||
|
||||
# THEN: urlopen should have been called CONNECTION_RETRIES + 1 count
|
||||
self.assertEquals(mocked_urlopen.call_count, CONNECTION_RETRIES + 1,
|
||||
'get_web_page() should have tried {} times'.format(CONNECTION_RETRIES))
|
||||
# THEN: urlopen should have been called CONNECTION_RETRIES + 1 count
|
||||
assert mocked_requests.get.call_count == CONNECTION_RETRIES, \
|
||||
'get should have been called {} times, but was only called {} times'.format(
|
||||
CONNECTION_RETRIES, mocked_requests.get.call_count)
|
||||
|
|
|
@ -34,7 +34,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm
|
|||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
FAKE_CONFIG = b"""
|
||||
FAKE_CONFIG = """
|
||||
[general]
|
||||
base url = http://example.com/frw/
|
||||
[songs]
|
||||
|
@ -45,7 +45,7 @@ directory = bibles
|
|||
directory = themes
|
||||
"""
|
||||
|
||||
FAKE_BROKEN_CONFIG = b"""
|
||||
FAKE_BROKEN_CONFIG = """
|
||||
[general]
|
||||
base url = http://example.com/frw/
|
||||
[songs]
|
||||
|
@ -54,7 +54,7 @@ directory = songs
|
|||
directory = bibles
|
||||
"""
|
||||
|
||||
FAKE_INVALID_CONFIG = b"""
|
||||
FAKE_INVALID_CONFIG = """
|
||||
<html>
|
||||
<head><title>This is not a config file</title></head>
|
||||
<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.gettempdir') as mocked_gettempdir, \
|
||||
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.value.return_value = True
|
||||
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:
|
||||
first_time_form = FirstTimeForm(None)
|
||||
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
|
||||
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:
|
||||
first_time_form = FirstTimeForm(None)
|
||||
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
|
||||
first_time_form._download_index()
|
||||
|
@ -225,14 +225,13 @@ class TestFirstTimeForm(TestCase, TestMixin):
|
|||
# GIVEN: Initial setup and mocks
|
||||
first_time_form = FirstTimeForm(None)
|
||||
first_time_form.initialize(MagicMock())
|
||||
mocked_get_web_page.side_effect = urllib.error.HTTPError(url='http//localhost',
|
||||
code=407,
|
||||
msg='Network proxy error',
|
||||
hdrs=None,
|
||||
fp=None)
|
||||
mocked_get_web_page.side_effect = ConnectionError('')
|
||||
mocked_message_box.Ok = 'OK'
|
||||
|
||||
# WHEN: the First Time Wizard calls to get the initial configuration
|
||||
first_time_form._download_index()
|
||||
|
||||
# THEN: the critical_error_message_box should have been called
|
||||
self.assertEquals(mocked_message_box.mock_calls[1][1][0], 'Network Error 407',
|
||||
'first_time_form should have caught Network Error')
|
||||
mocked_message_box.critical.assert_called_once_with(
|
||||
first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve '
|
||||
'initial configuration information', 'OK')
|
||||
|
|
|
@ -27,10 +27,10 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
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.ui import MainDisplay, AudioPlayer
|
||||
from openlp.core.ui.media import MediaController
|
||||
from openlp.core.ui.maindisplay import TRANSPARENT_STYLESHEET, OPAQUE_STYLESHEET
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
@ -184,7 +184,7 @@ class TestMainDisplay(TestCase, TestMixin):
|
|||
self.assertEqual(pyobjc_nsview.window().collectionBehavior(), 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):
|
||||
# GIVEN: Mocked show_display, setting for logo visibility
|
||||
display = MagicMock()
|
||||
|
@ -204,7 +204,7 @@ class TestMainDisplay(TestCase, TestMixin):
|
|||
# THEN: setVisible should had been called 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):
|
||||
# GIVEN: Mocked show_display, setting for logo visibility
|
||||
display = MagicMock()
|
||||
|
@ -224,8 +224,8 @@ class TestMainDisplay(TestCase, TestMixin):
|
|||
# THEN: setVisible should had not been called
|
||||
main_display.setVisible.assert_not_called()
|
||||
|
||||
@patch(u'openlp.core.ui.maindisplay.Settings')
|
||||
@patch(u'openlp.core.ui.maindisplay.build_html')
|
||||
@patch('openlp.core.ui.maindisplay.Settings')
|
||||
@patch('openlp.core.ui.maindisplay.build_html')
|
||||
def test_build_html_no_video(self, MockedSettings, Mocked_build_html):
|
||||
# GIVEN: Mocked display
|
||||
display = MagicMock()
|
||||
|
@ -252,8 +252,8 @@ class TestMainDisplay(TestCase, TestMixin):
|
|||
self.assertEquals(main_display.media_controller.video.call_count, 0,
|
||||
'Media Controller video should not have been called')
|
||||
|
||||
@patch(u'openlp.core.ui.maindisplay.Settings')
|
||||
@patch(u'openlp.core.ui.maindisplay.build_html')
|
||||
@patch('openlp.core.ui.maindisplay.Settings')
|
||||
@patch('openlp.core.ui.maindisplay.build_html')
|
||||
def test_build_html_video(self, MockedSettings, Mocked_build_html):
|
||||
# GIVEN: Mocked display
|
||||
display = MagicMock()
|
||||
|
@ -270,7 +270,7 @@ class TestMainDisplay(TestCase, TestMixin):
|
|||
service_item.theme_data = MagicMock()
|
||||
service_item.theme_data.background_type = 'video'
|
||||
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()
|
||||
display.plugin_manager = PluginManager()
|
||||
display.plugin_manager.plugins = [mocked_plugin]
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
# -*- 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 :mod:`~openlp.core.ui.style` module.
|
||||
"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import openlp.core.ui.style
|
||||
from openlp.core.ui.style import MEDIA_MANAGER_STYLE, WIN_REPAIR_STYLESHEET, get_application_stylesheet, \
|
||||
get_library_stylesheet
|
||||
|
||||
|
||||
@patch('openlp.core.ui.style.HAS_DARK_STYLE', True)
|
||||
@patch('openlp.core.ui.style.Settings')
|
||||
@patch.object(openlp.core.ui.style, 'qdarkstyle')
|
||||
def test_get_application_stylesheet_dark(mocked_qdarkstyle, MockSettings):
|
||||
"""Test that the dark stylesheet is returned when available and enabled"""
|
||||
# GIVEN: We're on Windows and no dark style is set
|
||||
mocked_settings = MagicMock()
|
||||
mocked_settings.value.return_value = True
|
||||
MockSettings.return_value = mocked_settings
|
||||
mocked_qdarkstyle.load_stylesheet_pyqt5.return_value = 'dark_style'
|
||||
|
||||
# WHEN: can_show_icon() is called
|
||||
result = get_application_stylesheet()
|
||||
|
||||
# THEN: the result should be false
|
||||
assert result == 'dark_style'
|
||||
|
||||
|
||||
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False)
|
||||
@patch('openlp.core.ui.style.is_win')
|
||||
@patch('openlp.core.ui.style.Settings')
|
||||
@patch('openlp.core.ui.style.Registry')
|
||||
def test_get_application_stylesheet_not_alternate_rows(MockRegistry, MockSettings, mocked_is_win):
|
||||
"""Test that the alternate rows stylesheet is returned when enabled in settings"""
|
||||
# GIVEN: We're on Windows and no dark style is set
|
||||
mocked_is_win.return_value = False
|
||||
MockSettings.return_value.value.return_value = False
|
||||
MockRegistry.return_value.get.return_value.palette.return_value.color.return_value.name.return_value = 'color'
|
||||
|
||||
# WHEN: can_show_icon() is called
|
||||
result = get_application_stylesheet()
|
||||
|
||||
# THEN: the result should be false
|
||||
MockSettings.return_value.value.assert_called_once_with('advanced/alternate rows')
|
||||
assert result == 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: color;}\n', result
|
||||
|
||||
|
||||
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False)
|
||||
@patch('openlp.core.ui.style.is_win')
|
||||
@patch('openlp.core.ui.style.Settings')
|
||||
def test_get_application_stylesheet_win_repair(MockSettings, mocked_is_win):
|
||||
"""Test that the Windows repair stylesheet is returned when on Windows"""
|
||||
# GIVEN: We're on Windows and no dark style is set
|
||||
mocked_is_win.return_value = True
|
||||
MockSettings.return_value.value.return_value = True
|
||||
|
||||
# WHEN: can_show_icon() is called
|
||||
result = get_application_stylesheet()
|
||||
|
||||
# THEN: the result should be false
|
||||
MockSettings.return_value.value.assert_called_once_with('advanced/alternate rows')
|
||||
assert result == WIN_REPAIR_STYLESHEET
|
||||
|
||||
|
||||
@patch('openlp.core.ui.style.HAS_DARK_STYLE', False)
|
||||
@patch('openlp.core.ui.style.Settings')
|
||||
def test_get_library_stylesheet_no_dark_style(MockSettings):
|
||||
"""Test that the media manager stylesheet is returned when there's no dark theme available"""
|
||||
# GIVEN: No dark style
|
||||
MockSettings.return_value.value.return_value = False
|
||||
|
||||
# WHEN: get_library_stylesheet() is called
|
||||
result = get_library_stylesheet()
|
||||
|
||||
# THEN: The correct stylesheet should be returned
|
||||
assert result == MEDIA_MANAGER_STYLE
|
||||
|
||||
|
||||
@patch('openlp.core.ui.style.HAS_DARK_STYLE', True)
|
||||
@patch('openlp.core.ui.style.Settings')
|
||||
def test_get_library_stylesheet_dark_style(MockSettings):
|
||||
"""Test that no stylesheet is returned when the dark theme is enabled"""
|
||||
# GIVEN: No dark style
|
||||
MockSettings.return_value.value.return_value = True
|
||||
|
||||
# WHEN: get_library_stylesheet() is called
|
||||
result = get_library_stylesheet()
|
||||
|
||||
# THEN: The correct stylesheet should be returned
|
||||
assert result == ''
|
|
@ -49,5 +49,5 @@ class TestThemeManager(TestCase):
|
|||
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
|
||||
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()
|
||||
|
|
|
@ -30,8 +30,9 @@ from unittest.mock import ANY, MagicMock, patch
|
|||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.ui import ThemeManager
|
||||
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
|
||||
|
||||
|
@ -57,13 +58,13 @@ class TestThemeManager(TestCase):
|
|||
"""
|
||||
# GIVEN: A new ThemeManager instance.
|
||||
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, \
|
||||
patch('zipfile.ZipFile.write') as mocked_zipfile_write:
|
||||
mocked_zipfile_init.return_value = None
|
||||
|
||||
# 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
|
||||
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
|
||||
"""
|
||||
# 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.
|
||||
with patch('builtins.open') as mocked_open, \
|
||||
patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
|
||||
with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
|
||||
patch('openlp.core.ui.thememanager.check_directory_exists'):
|
||||
mocked_open.return_value = MagicMock()
|
||||
theme_manager = ThemeManager(None)
|
||||
theme_manager.old_background_image = None
|
||||
theme_manager.generate_and_save_image = MagicMock()
|
||||
theme_manager.path = ''
|
||||
theme_manager.theme_path = MagicMock()
|
||||
mocked_theme = MagicMock()
|
||||
mocked_theme.theme_name = 'themename'
|
||||
mocked_theme.extract_formatted_xml = MagicMock()
|
||||
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
|
||||
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
|
||||
# Do replacement from end of string to avoid problems with path start
|
||||
file_name2 = file_name1[::-1].replace(os.sep, os.sep + os.sep, 2)[::-1]
|
||||
theme_manager._write_theme(mocked_theme, file_name1, file_name2)
|
||||
file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
|
||||
theme_manager._write_theme(mocked_theme, file_name1, file_name1)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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.
|
||||
with patch('builtins.open') as mocked_open, \
|
||||
patch('openlp.core.ui.thememanager.shutil.copyfile') as mocked_copyfile, \
|
||||
with patch('openlp.core.ui.thememanager.copyfile') as mocked_copyfile, \
|
||||
patch('openlp.core.ui.thememanager.check_directory_exists'):
|
||||
mocked_open.return_value = MagicMock()
|
||||
theme_manager = ThemeManager(None)
|
||||
theme_manager.old_background_image = None
|
||||
theme_manager.generate_and_save_image = MagicMock()
|
||||
theme_manager.path = ''
|
||||
theme_manager.theme_path = MagicMock()
|
||||
mocked_theme = MagicMock()
|
||||
mocked_theme.theme_name = 'themename'
|
||||
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
|
||||
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
|
||||
file_name2 = os.path.join(TEST_RESOURCES_PATH, 'church2.jpg')
|
||||
file_name1 = Path(TEST_RESOURCES_PATH, 'church.jpg')
|
||||
file_name2 = Path(TEST_RESOURCES_PATH, 'church2.jpg')
|
||||
theme_manager._write_theme(mocked_theme, file_name1, file_name2)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
|
@ -146,7 +139,7 @@ class TestThemeManager(TestCase):
|
|||
theme_manager = ThemeManager(None)
|
||||
theme_manager.old_background_image = None
|
||||
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.theme_name = 'theme 愛 name'
|
||||
mocked_theme.export_theme.return_value = "{}"
|
||||
|
@ -208,17 +201,17 @@ class TestThemeManager(TestCase):
|
|||
theme_manager = ThemeManager(None)
|
||||
theme_manager._create_theme_from_xml = MagicMock()
|
||||
theme_manager.generate_and_save_image = MagicMock()
|
||||
theme_manager.path = ''
|
||||
folder = mkdtemp()
|
||||
theme_file = os.path.join(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
|
||||
theme_manager.theme_path = None
|
||||
folder = Path(mkdtemp())
|
||||
theme_file = Path(TEST_RESOURCES_PATH, 'themes', 'Moss_on_tree.otz')
|
||||
|
||||
# WHEN: We try to unzip it
|
||||
theme_manager.unzip_theme(theme_file, folder)
|
||||
|
||||
# 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')
|
||||
shutil.rmtree(folder)
|
||||
shutil.rmtree(str(folder))
|
||||
|
||||
def test_unzip_theme_invalid_version(self):
|
||||
"""
|
||||
|
|
|
@ -58,7 +58,7 @@ class TestImageMediaItem(TestCase):
|
|||
Test that the validate_and_load_test() method when called without a group
|
||||
"""
|
||||
# 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
|
||||
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,
|
||||
# the directory should have been saved to the settings
|
||||
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.Settings')
|
||||
|
@ -75,7 +75,7 @@ class TestImageMediaItem(TestCase):
|
|||
Test that the validate_and_load_test() method when called with a group
|
||||
"""
|
||||
# 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
|
||||
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,
|
||||
# the directory should have been saved to the settings
|
||||
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')
|
||||
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
|
||||
"""
|
||||
# GIVEN: A list with 1 image and a mocked out manager
|
||||
image_list = ['test_image.jpg']
|
||||
ImageFilenames.filename = ''
|
||||
image_list = [Path('test_image.jpg')]
|
||||
ImageFilenames.file_path = None
|
||||
self.media_item.manager = MagicMock()
|
||||
|
||||
# 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')
|
||||
|
||||
# CLEANUP: Remove added attribute from ImageFilenames
|
||||
delattr(ImageFilenames, 'filename')
|
||||
delattr(ImageFilenames, 'file_path')
|
||||
|
||||
@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):
|
||||
|
@ -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
|
||||
"""
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
"""
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
"""
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
self.media_item.manager = MagicMock()
|
||||
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.id = 1
|
||||
|
||||
|
@ -215,13 +215,13 @@ class TestImageMediaItem(TestCase):
|
|||
# Create some fake objects that should be removed
|
||||
returned_object1 = ImageFilenames()
|
||||
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.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.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]
|
||||
if args[1] == ImageGroups and args[2]:
|
||||
# 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.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.service_path = ''
|
||||
self.media_item.service_path = Path()
|
||||
self.media_item.list_view = MagicMock()
|
||||
mocked_row_item = MagicMock()
|
||||
mocked_row_item.data.return_value = test_image
|
||||
|
@ -265,13 +265,13 @@ class TestImageMediaItem(TestCase):
|
|||
# GIVEN: An ImageFilenames that already exists in the database
|
||||
image_file = ImageFilenames()
|
||||
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.get_object_filtered.return_value = image_file
|
||||
ImageFilenames.filename = ''
|
||||
ImageFilenames.file_path = None
|
||||
|
||||
# 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
|
||||
self.assertIsInstance(item, QtWidgets.QTreeWidgetItem)
|
||||
|
@ -279,4 +279,4 @@ class TestImageMediaItem(TestCase):
|
|||
item_data = item.data(0, QtCore.Qt.UserRole)
|
||||
self.assertIsInstance(item_data, ImageFilenames)
|
||||
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)
|
||||
|
|
|
@ -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)
|
|
@ -24,13 +24,12 @@ Functional tests to test the Impress class and related methods.
|
|||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.plugins.presentations.lib.impresscontroller import \
|
||||
ImpressController, ImpressDocument, TextType
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.presentations.lib.impresscontroller import ImpressController, ImpressDocument, TextType
|
||||
from openlp.plugins.presentations.presentationplugin import __default_settings__
|
||||
|
||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||
|
@ -82,7 +81,7 @@ class TestImpressDocument(TestCase):
|
|||
mocked_plugin = MagicMock()
|
||||
mocked_plugin.settings_section = 'presentations'
|
||||
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.doc = ImpressDocument(self.ppc, self.file_name)
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ from unittest import TestCase
|
|||
from unittest.mock import patch, MagicMock, call
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
@ -92,17 +93,18 @@ class TestMediaItem(TestCase, TestMixin):
|
|||
"""
|
||||
# GIVEN: A mocked controller, and mocked os.path.getmtime
|
||||
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.supports = ['tmp']
|
||||
self.media_item.controllers = {
|
||||
'Mocked': mocked_controller
|
||||
}
|
||||
presentation_file = 'file.tmp'
|
||||
with patch('openlp.plugins.presentations.lib.mediaitem.os.path.getmtime') as mocked_getmtime, \
|
||||
patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists:
|
||||
mocked_getmtime.side_effect = [100, 200]
|
||||
mocked_exists.return_value = True
|
||||
|
||||
thmub_path = MagicMock(st_mtime=100)
|
||||
file_path = MagicMock(st_mtime=400)
|
||||
with patch.object(Path, 'stat', side_effect=[thmub_path, file_path]), \
|
||||
patch.object(Path, 'exists', return_value=True):
|
||||
presentation_file = Path('file.tmp')
|
||||
|
||||
# WHEN: calling clean_up_thumbnails
|
||||
self.media_item.clean_up_thumbnails(presentation_file, True)
|
||||
|
@ -123,9 +125,8 @@ class TestMediaItem(TestCase, TestMixin):
|
|||
self.media_item.controllers = {
|
||||
'Mocked': mocked_controller
|
||||
}
|
||||
presentation_file = 'file.tmp'
|
||||
with patch('openlp.plugins.presentations.lib.mediaitem.os.path.exists') as mocked_exists:
|
||||
mocked_exists.return_value = False
|
||||
presentation_file = Path('file.tmp')
|
||||
with patch.object(Path, 'exists', return_value=False):
|
||||
|
||||
# WHEN: calling clean_up_thumbnails
|
||||
self.media_item.clean_up_thumbnails(presentation_file, True)
|
||||
|
|
|
@ -32,6 +32,7 @@ from PyQt5 import QtCore, QtGui
|
|||
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PdfController, PdfDocument
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.lib import ScreenList
|
||||
|
||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||
|
@ -66,8 +67,8 @@ class TestPdfController(TestCase, TestMixin):
|
|||
self.desktop.screenGeometry.return_value = SCREEN['size']
|
||||
self.screens = ScreenList.create(self.desktop)
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.temp_folder = mkdtemp()
|
||||
self.thumbnail_folder = mkdtemp()
|
||||
self.temp_folder = Path(mkdtemp())
|
||||
self.thumbnail_folder = Path(mkdtemp())
|
||||
self.mock_plugin = MagicMock()
|
||||
self.mock_plugin.settings_section = self.temp_folder
|
||||
|
||||
|
@ -77,8 +78,8 @@ class TestPdfController(TestCase, TestMixin):
|
|||
"""
|
||||
del self.screens
|
||||
self.destroy_settings()
|
||||
shutil.rmtree(self.thumbnail_folder)
|
||||
shutil.rmtree(self.temp_folder)
|
||||
shutil.rmtree(str(self.thumbnail_folder))
|
||||
shutil.rmtree(str(self.temp_folder))
|
||||
|
||||
def test_constructor(self):
|
||||
"""
|
||||
|
@ -98,7 +99,7 @@ class TestPdfController(TestCase, TestMixin):
|
|||
Test loading of a Pdf using the PdfController
|
||||
"""
|
||||
# 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
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
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
|
||||
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
|
||||
if controller.gsbin:
|
||||
self.assertEqual(760, image.height(), 'The height should be 760')
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"""
|
||||
This module contains tests for the pptviewcontroller module of the Presentations plugin.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import mkdtemp
|
||||
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.core.common import is_win
|
||||
from openlp.core.common.path import Path
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
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
|
||||
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()
|
||||
|
||||
# WHEN reading the titles and notes
|
||||
|
@ -201,13 +201,13 @@ class TestPptviewDocument(TestCase):
|
|||
"""
|
||||
# GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file
|
||||
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 \
|
||||
mocked_dir_exists:
|
||||
mocked_exists.return_value = False
|
||||
mocked_path_exists.return_value = False
|
||||
mocked_dir_exists.return_value = False
|
||||
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()
|
||||
|
||||
# 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.
|
||||
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.')
|
||||
|
||||
def test_create_titles_and_notes_invalid_file(self):
|
||||
|
@ -228,7 +228,7 @@ class TestPptviewDocument(TestCase):
|
|||
mocked_is_zf.return_value = False
|
||||
mocked_open.filesize = 10
|
||||
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()
|
||||
|
||||
# WHEN: reading the titles and notes
|
||||
|
|
|
@ -23,9 +23,8 @@
|
|||
Functional tests to test the PresentationController and PresentationDocument
|
||||
classes and related methods.
|
||||
"""
|
||||
import os
|
||||
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.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
|
||||
"""
|
||||
# GIVEN: two lists of length==2 and a mocked open and get_thumbnail_folder
|
||||
mocked_open = mock_open()
|
||||
with patch('builtins.open', mocked_open), patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
|
||||
with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.write_text') as mocked_write_text, \
|
||||
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
|
||||
titles = ['uno', 'dos']
|
||||
notes = ['one', 'two']
|
||||
|
||||
# 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)
|
||||
|
||||
# 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')
|
||||
mocked_open.assert_any_call(os.path.join('test', 'slideNotes1.txt'), mode='wt', encoding='utf-8')
|
||||
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')
|
||||
self.assertEqual(mocked_write_text.call_count, 3, 'There should be exactly three files written')
|
||||
mocked_write_text.assert_has_calls([call('uno\ndos'), call('one'), call('two')])
|
||||
|
||||
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
|
||||
|
||||
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('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
|
||||
mocked_get_thumbnail_folder.return_value = 'test'
|
||||
patch('openlp.plugins.presentations.lib.presentationcontroller.Path.exists') as mocked_exists:
|
||||
mocked_get_thumbnail_folder.return_value = Path('test')
|
||||
mocked_exists.return_value = True
|
||||
|
||||
# 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.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(mocked_open.call_count, 3, 'Three files should be opened')
|
||||
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')
|
||||
self.assertEqual(mocked_read_text.call_count, 3, 'Three files should be read')
|
||||
|
||||
def test_get_titles_and_notes_with_file_not_found(self):
|
||||
"""
|
||||
Test PresentationDocument.get_titles_and_notes method with file not found
|
||||
"""
|
||||
# GIVEN: A mocked open, get_thumbnail_folder and exists
|
||||
with patch('builtins.open') as mocked_open, \
|
||||
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
|
||||
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
|
||||
mocked_get_thumbnail_folder.return_value = 'test'
|
||||
mocked_exists.return_value = False
|
||||
with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
|
||||
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
|
||||
mocked_read_text.side_effect = FileNotFoundError()
|
||||
mocked_get_thumbnail_folder.return_value = Path('test')
|
||||
|
||||
# WHEN: calling get_titles_and_notes
|
||||
result_titles, result_notes = self.document.get_titles_and_notes()
|
||||
|
||||
# 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.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(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):
|
||||
"""
|
||||
Test PresentationDocument.get_titles_and_notes method with file errors
|
||||
"""
|
||||
# GIVEN: A mocked open, get_thumbnail_folder and exists
|
||||
with patch('builtins.open') as mocked_open, \
|
||||
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder, \
|
||||
patch('openlp.plugins.presentations.lib.presentationcontroller.os.path.exists') as mocked_exists:
|
||||
mocked_get_thumbnail_folder.return_value = 'test'
|
||||
mocked_exists.return_value = True
|
||||
mocked_open.side_effect = IOError()
|
||||
with patch('openlp.plugins.presentations.lib.presentationcontroller.Path.read_text') as mocked_read_text, \
|
||||
patch(FOLDER_TO_PATCH) as mocked_get_thumbnail_folder:
|
||||
mocked_read_text.side_effect = IOError()
|
||||
mocked_get_thumbnail_folder.return_value = Path('test')
|
||||
|
||||
# WHEN: calling 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')
|
||||
self.get_thumbnail_folder_patcher = \
|
||||
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument.get_thumbnail_folder')
|
||||
self.os_patcher = patch('openlp.plugins.presentations.lib.presentationcontroller.os')
|
||||
self._setup_patcher = \
|
||||
patch('openlp.plugins.presentations.lib.presentationcontroller.PresentationDocument._setup')
|
||||
|
||||
self.mock_check_directory_exists = self.check_directory_exists_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_controller = MagicMock()
|
||||
|
||||
self.mock_get_thumbnail_folder.return_value = 'returned/path/'
|
||||
self.mock_get_thumbnail_folder.return_value = Path('returned/path/')
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
|
@ -199,7 +183,6 @@ class TestPresentationDocument(TestCase):
|
|||
"""
|
||||
self.check_directory_exists_patcher.stop()
|
||||
self.get_thumbnail_folder_patcher.stop()
|
||||
self.os_patcher.stop()
|
||||
self._setup_patcher.stop()
|
||||
|
||||
def test_initialise_presentation_document(self):
|
||||
|
@ -227,7 +210,7 @@ class TestPresentationDocument(TestCase):
|
|||
PresentationDocument(self.mock_controller, 'Name')
|
||||
|
||||
# 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()
|
||||
|
||||
|
@ -244,20 +227,3 @@ class TestPresentationDocument(TestCase):
|
|||
|
||||
# THEN: 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')
|
||||
|
|
|
@ -1,21 +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 #
|
||||
###############################################################################
|
|
@ -24,11 +24,11 @@ Package to test the openlp.core.__init__ package.
|
|||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core import OpenLP, parse_options
|
||||
from openlp.core import OpenLP
|
||||
from openlp.core.common import Settings
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
@ -96,9 +96,9 @@ class TestInit(TestCase, TestMixin):
|
|||
'build': 'bzr000'
|
||||
}
|
||||
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:
|
||||
mocked_get_application_version.return_value = MOCKED_VERSION
|
||||
mocked_get_version.return_value = MOCKED_VERSION
|
||||
mocked_question.return_value = QtWidgets.QMessageBox.No
|
||||
|
||||
# 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')
|
||||
self.openlp.splash = MagicMock()
|
||||
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:
|
||||
mocked_get_application_version.return_value = MOCKED_VERSION
|
||||
mocked_get_version.return_value = MOCKED_VERSION
|
||||
mocked_question.return_value = QtWidgets.QMessageBox.No
|
||||
|
||||
# WHEN: We check if a backup should be created
|
||||
|
|
|
@ -94,4 +94,3 @@ class TestPluginManager(TestCase, TestMixin):
|
|||
self.assertIn('custom', plugin_names, 'There should be a "custom" plugin')
|
||||
self.assertIn('songusage', plugin_names, 'There should be a "songusage" plugin')
|
||||
self.assertIn('alerts', plugin_names, 'There should be a "alerts" plugin')
|
||||
self.assertIn('remotes', plugin_names, 'There should be a "remotes" plugin')
|
||||
|
|
|
@ -26,7 +26,8 @@ from unittest import TestCase
|
|||
from unittest.mock import patch, MagicMock
|
||||
|
||||
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
|
||||
|
||||
|
@ -91,6 +92,23 @@ class TestThemeManager(TestCase, TestMixin):
|
|||
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'
|
||||
|
||||
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):
|
||||
"""
|
||||
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.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.
|
||||
"""
|
||||
# GIVEN:
|
||||
self.theme_manager.load_themes = MagicMock()
|
||||
self.theme_manager.path = MagicMock()
|
||||
self.theme_manager.theme_path = MagicMock()
|
||||
|
||||
# WHEN:
|
||||
self.theme_manager.bootstrap_post_set_up()
|
||||
|
||||
# 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")
|
||||
|
|
|
@ -22,13 +22,15 @@
|
|||
"""
|
||||
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 openlp.core.common import Registry
|
||||
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):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -38,6 +40,7 @@ class TestBibleHTTP(TestCase):
|
|||
Registry.create()
|
||||
Registry().register('service_list', MagicMock())
|
||||
Registry().register('application', MagicMock())
|
||||
Registry().register('main_window', MagicMock())
|
||||
|
||||
def test_bible_gateway_extract_books(self):
|
||||
"""
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue