forked from openlp/openlp
Head
This commit is contained in:
commit
a41d86cb7c
@ -27,26 +27,26 @@ 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 os
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
from traceback import format_exception
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from traceback import format_exception
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists, \
|
||||
is_macosx, is_win, translate
|
||||
from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
|
||||
check_directory_exists, is_macosx, is_win, translate
|
||||
from openlp.core.common.versionchecker import VersionThread, get_application_version
|
||||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.resources import qInitResources
|
||||
from openlp.core.ui.mainwindow import MainWindow
|
||||
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
|
||||
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||
from openlp.core.ui.exceptionform import ExceptionForm
|
||||
from openlp.core.ui import SplashScreen
|
||||
from openlp.core.utils import LanguageManager, VersionThread, get_application_version
|
||||
|
||||
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
|
||||
|
||||
__all__ = ['OpenLP', 'main']
|
||||
|
||||
|
@ -24,15 +24,15 @@ The :mod:`common` module contains most of the components and libraries that make
|
||||
OpenLP work.
|
||||
"""
|
||||
import hashlib
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||
from codecs import decode, encode
|
||||
from shutil import which
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import QCryptographicHash as QHash
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
@ -40,6 +40,9 @@ log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
|
||||
SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
|
||||
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
|
||||
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
|
||||
IMAGES_FILTER = None
|
||||
|
||||
|
||||
def trace_error_handler(logger):
|
||||
@ -241,4 +244,130 @@ from .registryproperties import RegistryProperties
|
||||
from .uistrings import UiStrings
|
||||
from .settings import Settings
|
||||
from .applocation import AppLocation
|
||||
from .historycombobox import HistoryComboBox
|
||||
from .actions import ActionList
|
||||
from .languagemanager import LanguageManager
|
||||
|
||||
|
||||
def add_actions(target, actions):
|
||||
"""
|
||||
Adds multiple actions to a menu or toolbar in one command.
|
||||
|
||||
:param target: The menu or toolbar to add actions to
|
||||
:param actions: The actions to be added. An action consisting of the keyword ``None``
|
||||
will result in a separator being inserted into the target.
|
||||
"""
|
||||
for action in actions:
|
||||
if action is None:
|
||||
target.addSeparator()
|
||||
else:
|
||||
target.addAction(action)
|
||||
|
||||
|
||||
def get_uno_command(connection_type='pipe'):
|
||||
"""
|
||||
Returns the UNO command to launch an libreoffice.org instance.
|
||||
"""
|
||||
for command in ['libreoffice', 'soffice']:
|
||||
if which(command):
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError('Command not found')
|
||||
|
||||
OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
if connection_type == 'pipe':
|
||||
CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
|
||||
else:
|
||||
CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
|
||||
return '%s %s %s' % (command, OPTIONS, CONNECTION)
|
||||
|
||||
|
||||
def get_uno_instance(resolver, connection_type='pipe'):
|
||||
"""
|
||||
Returns a running libreoffice.org instance.
|
||||
|
||||
:param resolver: The UNO resolver to use to find a running instance.
|
||||
"""
|
||||
log.debug('get UNO Desktop Openoffice - resolve')
|
||||
if connection_type == 'pipe':
|
||||
return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
|
||||
else:
|
||||
return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
"""
|
||||
Returns the name of the encoding used to convert Unicode filenames into system file names.
|
||||
"""
|
||||
encoding = sys.getfilesystemencoding()
|
||||
if encoding is None:
|
||||
encoding = sys.getdefaultencoding()
|
||||
return encoding
|
||||
|
||||
|
||||
def split_filename(path):
|
||||
"""
|
||||
Return a list of the parts in a given path.
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
if not os.path.isfile(path):
|
||||
return path, ''
|
||||
else:
|
||||
return os.path.split(path)
|
||||
|
||||
|
||||
def delete_file(file_path_name):
|
||||
"""
|
||||
Deletes a file from the system.
|
||||
|
||||
:param file_path_name: The file, including path, to delete.
|
||||
"""
|
||||
if not file_path_name:
|
||||
return False
|
||||
try:
|
||||
if os.path.exists(file_path_name):
|
||||
os.remove(file_path_name)
|
||||
return True
|
||||
except (IOError, OSError):
|
||||
log.exception("Unable to delete file %s" % file_path_name)
|
||||
return False
|
||||
|
||||
|
||||
def get_images_filter():
|
||||
"""
|
||||
Returns a filter string for a file dialog containing all the supported image formats.
|
||||
"""
|
||||
global IMAGES_FILTER
|
||||
if not IMAGES_FILTER:
|
||||
log.debug('Generating images filter.')
|
||||
formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
|
||||
visible_formats = '(*.%s)' % '; *.'.join(formats)
|
||||
actual_formats = '(*.%s)' % ' *.'.join(formats)
|
||||
IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
|
||||
return IMAGES_FILTER
|
||||
|
||||
|
||||
def is_not_image_file(file_name):
|
||||
"""
|
||||
Validate that the file is not an image file.
|
||||
|
||||
:param file_name: File name to be checked.
|
||||
"""
|
||||
if not file_name:
|
||||
return True
|
||||
else:
|
||||
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
|
||||
file_part, file_extension = os.path.splitext(str(file_name))
|
||||
if file_extension[1:].lower() in formats and os.path.exists(file_name):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def clean_filename(filename):
|
||||
"""
|
||||
Removes invalid characters from the given ``filename``.
|
||||
|
||||
:param filename: The "dirty" file name to clean.
|
||||
"""
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, 'utf-8')
|
||||
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
|
||||
|
@ -22,9 +22,9 @@
|
||||
"""
|
||||
The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
|
||||
"""
|
||||
import locale
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
@ -33,6 +33,9 @@ from openlp.core.common import AppLocation, Settings, translate, is_win, is_maco
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ICU_COLLATOR = None
|
||||
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
|
||||
|
||||
|
||||
class LanguageManager(object):
|
||||
"""
|
||||
@ -144,3 +147,60 @@ class LanguageManager(object):
|
||||
if not LanguageManager.__qm_list__:
|
||||
LanguageManager.init_qm_list()
|
||||
return LanguageManager.__qm_list__
|
||||
|
||||
|
||||
def format_time(text, local_time):
|
||||
"""
|
||||
Workaround for Python built-in time formatting function time.strftime().
|
||||
|
||||
time.strftime() accepts only ascii characters. This function accepts
|
||||
unicode string and passes individual % placeholders to time.strftime().
|
||||
This ensures only ascii characters are passed to time.strftime().
|
||||
|
||||
:param text: The text to be processed.
|
||||
:param local_time: The time to be used to add to the string. This is a time object
|
||||
"""
|
||||
|
||||
def match_formatting(match):
|
||||
"""
|
||||
Format the match
|
||||
"""
|
||||
return local_time.strftime(match.group())
|
||||
|
||||
return re.sub('\%[a-zA-Z]', match_formatting, text)
|
||||
|
||||
|
||||
def get_locale_key(string):
|
||||
"""
|
||||
Creates a key for case insensitive, locale aware string sorting.
|
||||
|
||||
:param string: The corresponding string.
|
||||
"""
|
||||
string = string.lower()
|
||||
# ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
|
||||
global ICU_COLLATOR
|
||||
try:
|
||||
if ICU_COLLATOR is None:
|
||||
import icu
|
||||
language = LanguageManager.get_language()
|
||||
icu_locale = icu.Locale(language)
|
||||
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
|
||||
return ICU_COLLATOR.getSortKey(string)
|
||||
except:
|
||||
return locale.strxfrm(string).encode()
|
||||
|
||||
|
||||
def get_natural_key(string):
|
||||
"""
|
||||
Generate a key for locale aware natural string sorting.
|
||||
|
||||
:param string: string to be sorted by
|
||||
Returns a list of string compare keys and integers.
|
||||
"""
|
||||
key = DIGITS_OR_NONDIGITS.findall(string)
|
||||
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
|
||||
# Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
|
||||
# and int.
|
||||
if string and string[0].isdigit():
|
||||
return [b''] + key
|
||||
return key
|
@ -121,6 +121,7 @@ class Settings(QtCore.QSettings):
|
||||
'advanced/double click live': False,
|
||||
'advanced/enable exit confirmation': True,
|
||||
'advanced/expand service item': False,
|
||||
'advanced/slide max height': 0,
|
||||
'advanced/hide mouse': True,
|
||||
'advanced/is portable': False,
|
||||
'advanced/max recent files': 20,
|
||||
@ -131,6 +132,7 @@ class Settings(QtCore.QSettings):
|
||||
'advanced/save current plugin': False,
|
||||
'advanced/slide limits': SlideLimits.End,
|
||||
'advanced/single click preview': False,
|
||||
'advanced/single click service preview': False,
|
||||
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
'advanced/search as type': True,
|
||||
'crashreport/last directory': '',
|
||||
|
170
openlp/core/common/versionchecker.py
Normal file
170
openlp/core/common/versionchecker.py
Normal file
@ -0,0 +1,170 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from distutils.version import LooseVersion
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from openlp.core.common import AppLocation, Settings
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
APPLICATION_VERSION = {}
|
||||
CONNECTION_TIMEOUT = 30
|
||||
CONNECTION_RETRIES = 2
|
||||
|
||||
|
||||
class VersionThread(QtCore.QThread):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
|
||||
:param main_window: The main window Object.
|
||||
"""
|
||||
log.debug("VersionThread - Initialise")
|
||||
super(VersionThread, self).__init__(None)
|
||||
self.main_window = main_window
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
self.sleep(1)
|
||||
log.debug('Version thread - run')
|
||||
app_version = get_application_version()
|
||||
version = check_latest_version(app_version)
|
||||
log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full']))))
|
||||
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
|
||||
self.main_window.openlp_version_check.emit('%s' % version)
|
||||
|
||||
|
||||
def get_application_version():
|
||||
"""
|
||||
Returns the application version of the running instance of OpenLP::
|
||||
|
||||
{'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
|
||||
"""
|
||||
global APPLICATION_VERSION
|
||||
if APPLICATION_VERSION:
|
||||
return APPLICATION_VERSION
|
||||
if '--dev-version' in sys.argv or '-d' in sys.argv:
|
||||
# NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
|
||||
# there.
|
||||
|
||||
# Get the revision of this tree.
|
||||
bzr = Popen(('bzr', 'revno'), stdout=PIPE)
|
||||
tree_revision, error = bzr.communicate()
|
||||
tree_revision = tree_revision.decode()
|
||||
code = bzr.wait()
|
||||
if code != 0:
|
||||
raise Exception('Error running bzr log')
|
||||
|
||||
# Get all tags.
|
||||
bzr = Popen(('bzr', 'tags'), stdout=PIPE)
|
||||
output, error = bzr.communicate()
|
||||
code = bzr.wait()
|
||||
if code != 0:
|
||||
raise Exception('Error running bzr tags')
|
||||
tags = list(map(bytes.decode, output.splitlines()))
|
||||
if not tags:
|
||||
tag_version = '0.0.0'
|
||||
tag_revision = '0'
|
||||
else:
|
||||
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
|
||||
# another series.
|
||||
tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
|
||||
# Get the last tag and split it in a revision and tag name.
|
||||
tag_version, tag_revision = tags[-1].split()
|
||||
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision
|
||||
# number in the full version.
|
||||
if tree_revision == tag_revision:
|
||||
full_version = tag_version.strip()
|
||||
else:
|
||||
full_version = '%s-bzr%s' % (tag_version.strip(), tree_revision.strip())
|
||||
else:
|
||||
# We're not running the development version, let's use the file.
|
||||
file_path = AppLocation.get_directory(AppLocation.VersionDir)
|
||||
file_path = os.path.join(file_path, '.version')
|
||||
version_file = None
|
||||
try:
|
||||
version_file = open(file_path, 'r')
|
||||
full_version = str(version_file.read()).rstrip()
|
||||
except IOError:
|
||||
log.exception('Error in version file.')
|
||||
full_version = '0.0.0-bzr000'
|
||||
finally:
|
||||
if version_file:
|
||||
version_file.close()
|
||||
bits = full_version.split('-')
|
||||
APPLICATION_VERSION = {
|
||||
'full': full_version,
|
||||
'version': bits[0],
|
||||
'build': bits[1] if len(bits) > 1 else None
|
||||
}
|
||||
if APPLICATION_VERSION['build']:
|
||||
log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build'])
|
||||
else:
|
||||
log.info('Openlp version %s' % 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/%s %s/%s; ' % (current_version['full'], platform.system(),
|
||||
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
|
@ -34,9 +34,8 @@ from sqlalchemy.pool import NullPool
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, translate
|
||||
from openlp.core.common import AppLocation, Settings, translate, delete_file
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.utils import delete_file
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,11 +24,10 @@ Provide the generic plugin functionality for OpenLP plugins.
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
|
||||
from openlp.core.utils import get_application_version
|
||||
from openlp.core.common.versionchecker import get_application_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -513,17 +513,13 @@ class PJLink1(QTcpSocket):
|
||||
log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip()))
|
||||
log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue))
|
||||
self.socket_timer.start()
|
||||
try:
|
||||
self.projectorNetwork.emit(S_NETWORK_SENDING)
|
||||
sent = self.write(out)
|
||||
sent = self.write(out.encode('ascii'))
|
||||
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
||||
if sent == -1:
|
||||
# Network error?
|
||||
self.change_status(E_NETWORK,
|
||||
translate('OpenLP.PJLink1', 'Error while sending data to projector'))
|
||||
except SocketError as e:
|
||||
self.disconnect_from_host(abort=True)
|
||||
self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString()))
|
||||
|
||||
def process_command(self, cmd, data):
|
||||
"""
|
||||
@ -665,7 +661,15 @@ class PJLink1(QTcpSocket):
|
||||
|
||||
:param data: Class that projector supports.
|
||||
"""
|
||||
self.pjlink_class = data
|
||||
# bug 1550891: Projector returns non-standard class response:
|
||||
# : Expected: %1CLSS=1
|
||||
# : Received: %1CLSS=Class 1
|
||||
if len(data) > 1:
|
||||
# Split non-standard information from response
|
||||
clss = data.split()[-1]
|
||||
else:
|
||||
clss = data
|
||||
self.pjlink_class = clss
|
||||
log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class))
|
||||
return
|
||||
|
||||
|
@ -27,8 +27,8 @@ import logging
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, UiStrings, translate, is_macosx
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.utils.actions import ActionList
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
183
openlp/core/lib/webpagereader.py
Normal file
183
openlp/core/lib/webpagereader.py
Normal file
@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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.utils` module provides the utility libraries for OpenLP.
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.client import HTTPException
|
||||
from random import randint
|
||||
|
||||
from openlp.core.common import Registry
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
USER_AGENTS = {
|
||||
'win32': [
|
||||
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36'
|
||||
],
|
||||
'darwin': [
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) '
|
||||
'Chrome/26.0.1410.43 Safari/537.31',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) '
|
||||
'Chrome/20.0.1132.57 Safari/536.11',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) '
|
||||
'Chrome/20.0.1132.47 Safari/536.11',
|
||||
],
|
||||
'linux2': [
|
||||
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 '
|
||||
'Chrome/25.0.1364.160 Safari/537.22',
|
||||
'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 '
|
||||
'Safari/537.11',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27'
|
||||
],
|
||||
'default': [
|
||||
'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0'
|
||||
]
|
||||
}
|
||||
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.
|
||||
"""
|
||||
browser_list = USER_AGENTS.get(sys.platform, None)
|
||||
if not browser_list:
|
||||
browser_list = USER_AGENTS['default']
|
||||
random_index = randint(0, len(browser_list) - 1)
|
||||
return browser_list[random_index]
|
||||
|
||||
|
||||
def get_web_page(url, header=None, update_openlp=False):
|
||||
"""
|
||||
Attempts to download the webpage at url and returns that page or None.
|
||||
|
||||
:param url: The URL to be downloaded.
|
||||
:param header: An optional HTTP header to pass in the request to the web server.
|
||||
: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])
|
||||
log.debug('Downloading URL = %s' % url)
|
||||
retries = 0
|
||||
while retries <= CONNECTION_RETRIES:
|
||||
retries += 1
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
|
||||
log.debug('Downloaded page {}'.format(page.geturl()))
|
||||
break
|
||||
except urllib.error.URLError as err:
|
||||
log.exception('URLError on {}'.format(url))
|
||||
log.exception('URLError: {}'.format(err.reason))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except socket.timeout:
|
||||
log.exception('Socket timeout: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except socket.gaierror:
|
||||
log.exception('Socket gaierror: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except ConnectionRefusedError:
|
||||
log.exception('ConnectionRefused: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
break
|
||||
except ConnectionError:
|
||||
log.exception('Connection error: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except HTTPException:
|
||||
log.exception('HTTPException error: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except:
|
||||
# Don't know what's happening, so reraise the original
|
||||
raise
|
||||
if update_openlp:
|
||||
Registry().get('application').process_events()
|
||||
if not page:
|
||||
log.exception('{} could not be downloaded'.format(url))
|
||||
return None
|
||||
log.debug(page)
|
||||
return page
|
||||
|
||||
|
||||
__all__ = ['get_application_version', 'check_latest_version',
|
||||
'get_web_page']
|
@ -26,8 +26,8 @@ import webbrowser
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common.versionchecker import get_application_version
|
||||
from openlp.core.lib import translate
|
||||
from openlp.core.utils import get_application_version
|
||||
from .aboutdialog import UiAboutDialog
|
||||
|
||||
|
||||
|
@ -29,9 +29,9 @@ import sys
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
|
||||
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate, get_images_filter
|
||||
from openlp.core.lib import ColorButton, SettingsTab, build_icon
|
||||
from openlp.core.utils import format_time, get_images_filter
|
||||
from openlp.core.common.languagemanager import format_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -77,9 +77,19 @@ class AdvancedTab(SettingsTab):
|
||||
self.single_click_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box)
|
||||
self.single_click_preview_check_box.setObjectName('single_click_preview_check_box')
|
||||
self.ui_layout.addRow(self.single_click_preview_check_box)
|
||||
self.single_click_service_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box)
|
||||
self.single_click_service_preview_check_box.setObjectName('single_click_service_preview_check_box')
|
||||
self.ui_layout.addRow(self.single_click_service_preview_check_box)
|
||||
self.expand_service_item_check_box = QtWidgets.QCheckBox(self.ui_group_box)
|
||||
self.expand_service_item_check_box.setObjectName('expand_service_item_check_box')
|
||||
self.ui_layout.addRow(self.expand_service_item_check_box)
|
||||
self.slide_max_height_label = QtWidgets.QLabel(self.ui_group_box)
|
||||
self.slide_max_height_label.setObjectName('slide_max_height_label')
|
||||
self.slide_max_height_spin_box = QtWidgets.QSpinBox(self.ui_group_box)
|
||||
self.slide_max_height_spin_box.setObjectName('slide_max_height_spin_box')
|
||||
self.slide_max_height_spin_box.setRange(0, 1000)
|
||||
self.slide_max_height_spin_box.setSingleStep(20)
|
||||
self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_spin_box)
|
||||
self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box)
|
||||
self.search_as_type_check_box.setObjectName('SearchAsType_check_box')
|
||||
self.ui_layout.addRow(self.search_as_type_check_box)
|
||||
@ -270,8 +280,13 @@ class AdvancedTab(SettingsTab):
|
||||
'Double-click to send items straight to live'))
|
||||
self.single_click_preview_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Preview items when clicked in Media Manager'))
|
||||
self.single_click_service_preview_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Preview items when clicked in Service Manager'))
|
||||
self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Expand new service items on creation'))
|
||||
self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab',
|
||||
'Max height for non-text slides\nin slide controller:'))
|
||||
self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled'))
|
||||
self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Enable application exit confirmation'))
|
||||
self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name'))
|
||||
@ -339,7 +354,9 @@ class AdvancedTab(SettingsTab):
|
||||
self.media_plugin_check_box.setChecked(settings.value('save current plugin'))
|
||||
self.double_click_live_check_box.setChecked(settings.value('double click live'))
|
||||
self.single_click_preview_check_box.setChecked(settings.value('single click preview'))
|
||||
self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview'))
|
||||
self.expand_service_item_check_box.setChecked(settings.value('expand service item'))
|
||||
self.slide_max_height_spin_box.setValue(settings.value('slide max height'))
|
||||
self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation'))
|
||||
self.hide_mouse_check_box.setChecked(settings.value('hide mouse'))
|
||||
self.service_name_day.setCurrentIndex(settings.value('default service day'))
|
||||
@ -420,7 +437,9 @@ class AdvancedTab(SettingsTab):
|
||||
settings.setValue('save current plugin', self.media_plugin_check_box.isChecked())
|
||||
settings.setValue('double click live', self.double_click_live_check_box.isChecked())
|
||||
settings.setValue('single click preview', self.single_click_preview_check_box.isChecked())
|
||||
settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked())
|
||||
settings.setValue('expand service item', self.expand_service_item_check_box.isChecked())
|
||||
settings.setValue('slide max height', self.slide_max_height_spin_box.value())
|
||||
settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked())
|
||||
settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked())
|
||||
settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked())
|
||||
|
@ -23,18 +23,17 @@
|
||||
The actual exception dialog form.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
||||
import bs4
|
||||
import sqlalchemy
|
||||
from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets
|
||||
from lxml import etree
|
||||
|
||||
from openlp.core.common import RegistryProperties, is_linux
|
||||
|
||||
from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets
|
||||
|
||||
try:
|
||||
import migrate
|
||||
MIGRATE_VERSION = getattr(migrate, '__version__', '< 0.7')
|
||||
@ -74,7 +73,7 @@ except ImportError:
|
||||
VLC_VERSION = '-'
|
||||
|
||||
from openlp.core.common import Settings, UiStrings, translate
|
||||
from openlp.core.utils import get_application_version
|
||||
from openlp.core.common.versionchecker import get_application_version
|
||||
|
||||
from .exceptiondialog import Ui_ExceptionDialog
|
||||
|
||||
@ -180,11 +179,13 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
||||
if ':' in line:
|
||||
exception = line.split('\n')[-1].split(':')[0]
|
||||
subject = 'Bug report: %s in %s' % (exception, source)
|
||||
mail_to_url = QtCore.QUrlQuery('mailto:bugs@openlp.org')
|
||||
mail_to_url.addQueryItem('subject', subject)
|
||||
mail_to_url.addQueryItem('body', self.report_text % content)
|
||||
mail_urlquery = QtCore.QUrlQuery()
|
||||
mail_urlquery.addQueryItem('subject', subject)
|
||||
mail_urlquery.addQueryItem('body', self.report_text % content)
|
||||
if self.file_attachment:
|
||||
mail_to_url.addQueryItem('attach', self.file_attachment)
|
||||
mail_urlquery.addQueryItem('attach', self.file_attachment)
|
||||
mail_to_url = QtCore.QUrl('mailto:bugs@openlp.org')
|
||||
mail_to_url.setQuery(mail_urlquery)
|
||||
QtGui.QDesktopServices.openUrl(mail_to_url)
|
||||
|
||||
def on_description_updated(self):
|
||||
|
@ -39,7 +39,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin
|
||||
translate, clean_button_text, trace_error_handler
|
||||
from openlp.core.lib import PluginStatus, build_icon
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.utils import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT
|
||||
from openlp.core.lib.webpagereader import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT
|
||||
from .firsttimewizard import UiFirstTimeWizard, FirstTimePage
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -25,7 +25,7 @@ The language selection dialog.
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.utils import LanguageManager
|
||||
from openlp.core.common import LanguageManager
|
||||
from .firsttimelanguagedialog import Ui_FirstTimeLanguageDialog
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`~openlp.core.common.historycombobox` module contains the HistoryComboBox widget
|
||||
The :mod:`~openlp.core.ui.lib.historycombobox` module contains the HistoryComboBox widget
|
||||
"""
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
@ -26,7 +26,7 @@ It is based on a QTableWidget but represents its contents in list form.
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import RegistryProperties
|
||||
from openlp.core.common import RegistryProperties, Settings
|
||||
from openlp.core.lib import ImageSource, ServiceItem
|
||||
|
||||
|
||||
@ -63,6 +63,8 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
|
||||
# Initialize variables.
|
||||
self.service_item = ServiceItem()
|
||||
self.screen_ratio = screen_ratio
|
||||
# Connect signals
|
||||
self.verticalHeader().sectionResized.connect(self.row_resized)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""
|
||||
@ -80,12 +82,30 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
|
||||
# Sort out songs, bibles, etc.
|
||||
if self.service_item.is_text():
|
||||
self.resizeRowsToContents()
|
||||
else:
|
||||
# Sort out image heights.
|
||||
for frame_number in range(len(self.service_item.get_frames())):
|
||||
else:
|
||||
height = self.viewport().width() // self.screen_ratio
|
||||
max_img_row_height = Settings().value('advanced/slide max height')
|
||||
# Adjust for row height cap if in use.
|
||||
if max_img_row_height > 0 and height > max_img_row_height:
|
||||
height = max_img_row_height
|
||||
# Apply new height to slides
|
||||
for frame_number in range(len(self.service_item.get_frames())):
|
||||
self.setRowHeight(frame_number, height)
|
||||
|
||||
def row_resized(self, row, old_height, new_height):
|
||||
"""
|
||||
Will scale non-image slides.
|
||||
"""
|
||||
# Only for non-text slides when row height cap in use
|
||||
if self.service_item.is_text() or Settings().value('advanced/slide max height') <= 0:
|
||||
return
|
||||
# Get and validate label widget containing slide & adjust max width
|
||||
try:
|
||||
self.cellWidget(row, 0).children()[1].setMaximumWidth(new_height * self.screen_ratio)
|
||||
except:
|
||||
return
|
||||
|
||||
def screen_size_changed(self, screen_ratio):
|
||||
"""
|
||||
This method is called whenever the live screen size changes, which then makes a layout recalculation necessary
|
||||
@ -139,8 +159,26 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
|
||||
pixmap = QtGui.QPixmap.fromImage(image)
|
||||
pixmap.setDevicePixelRatio(label.devicePixelRatio())
|
||||
label.setPixmap(pixmap)
|
||||
self.setCellWidget(frame_number, 0, label)
|
||||
slide_height = width // self.screen_ratio
|
||||
# Setup row height cap if in use.
|
||||
max_img_row_height = Settings().value('advanced/slide max height')
|
||||
if max_img_row_height > 0:
|
||||
if slide_height > max_img_row_height:
|
||||
slide_height = max_img_row_height
|
||||
label.setMaximumWidth(max_img_row_height * self.screen_ratio)
|
||||
label.resize(max_img_row_height * self.screen_ratio, max_img_row_height)
|
||||
# Build widget with stretch padding
|
||||
container = QtWidgets.QWidget()
|
||||
hbox = QtWidgets.QHBoxLayout()
|
||||
hbox.setContentsMargins(0, 0, 0, 0)
|
||||
hbox.addWidget(label, stretch=1)
|
||||
hbox.addStretch(0)
|
||||
container.setLayout(hbox)
|
||||
# Add to table
|
||||
self.setCellWidget(frame_number, 0, container)
|
||||
else:
|
||||
# Add to table
|
||||
self.setCellWidget(frame_number, 0, label)
|
||||
row += 1
|
||||
text.append(str(row))
|
||||
self.setItem(frame_number, 0, item)
|
||||
|
@ -408,10 +408,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
self.application.process_events()
|
||||
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
|
||||
if is_win():
|
||||
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
|
||||
fade_shake_timer.stop()
|
||||
elif is_win():
|
||||
self.shake_web_view()
|
||||
# Wait for the webview to update before getting the preview.
|
||||
# Important otherwise first preview will miss the background !
|
||||
while not self.web_loaded:
|
||||
@ -429,6 +426,9 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
self.setVisible(True)
|
||||
else:
|
||||
self.setVisible(True)
|
||||
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
|
||||
if is_win():
|
||||
self.shake_web_view()
|
||||
return self.grab()
|
||||
|
||||
def build_html(self, service_item, image_path=''):
|
||||
|
@ -24,30 +24,29 @@ This is the main window, where all the action happens.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from distutils import dir_util
|
||||
from distutils.errors import DistutilsFileError
|
||||
from tempfile import gettempdir
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate, \
|
||||
is_win, is_macosx
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \
|
||||
check_directory_exists, translate, is_win, is_macosx, add_actions
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.versionchecker import get_application_version
|
||||
from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \
|
||||
build_icon
|
||||
from openlp.core.lib.ui import UiStrings, create_action
|
||||
from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
|
||||
MediaDockManager, ShortcutListForm, FormattingTagForm, PreviewController
|
||||
|
||||
from openlp.core.ui.media import MediaController
|
||||
from openlp.core.utils import LanguageManager, add_actions, get_application_version
|
||||
from openlp.core.utils.actions import ActionList, CategoryOrder
|
||||
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||
from openlp.core.ui.projector.manager import ProjectorManager
|
||||
from openlp.core.ui.media import MediaController
|
||||
from openlp.core.ui.printserviceform import PrintServiceForm
|
||||
from openlp.core.ui.projector.manager import ProjectorManager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -83,60 +83,60 @@ class Ui_ProjectorManager(object):
|
||||
self.one_toolbar.add_toolbar_action('new_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Add Projector'),
|
||||
icon=':/projector/projector_new.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'),
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector.'),
|
||||
triggers=self.on_add_projector)
|
||||
# Show edit/delete when projector not connected
|
||||
self.one_toolbar.add_toolbar_action('edit_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Edit Projector'),
|
||||
icon=':/general/general_edit.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector'),
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector.'),
|
||||
triggers=self.on_edit_projector)
|
||||
self.one_toolbar.add_toolbar_action('delete_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Delete Projector'),
|
||||
icon=':/general/general_delete.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector'),
|
||||
tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector.'),
|
||||
triggers=self.on_delete_projector)
|
||||
# Show source/view when projector connected
|
||||
self.one_toolbar.add_toolbar_action('source_view_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Select Input Source'),
|
||||
icon=':/projector/projector_hdmi.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Choose input source on selected projector'),
|
||||
'Choose input source on selected projector.'),
|
||||
triggers=self.on_select_input)
|
||||
self.one_toolbar.add_toolbar_action('view_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'View Projector'),
|
||||
icon=':/system/system_about.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'View selected projector information'),
|
||||
'View selected projector information.'),
|
||||
triggers=self.on_status_projector)
|
||||
self.one_toolbar.addSeparator()
|
||||
self.one_toolbar.add_toolbar_action('connect_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projector'),
|
||||
'Connect to selected projector.'),
|
||||
icon=':/projector/projector_connect.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projector'),
|
||||
'Connect to selected projector.'),
|
||||
triggers=self.on_connect_projector)
|
||||
self.one_toolbar.add_toolbar_action('connect_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projectors'),
|
||||
icon=':/projector/projector_connect_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Connect to selected projector'),
|
||||
'Connect to selected projectors.'),
|
||||
triggers=self.on_connect_projector)
|
||||
self.one_toolbar.add_toolbar_action('disconnect_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projectors'),
|
||||
icon=':/projector/projector_disconnect.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projector'),
|
||||
'Disconnect from selected projector.'),
|
||||
triggers=self.on_disconnect_projector)
|
||||
self.one_toolbar.add_toolbar_action('disconnect_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projector'),
|
||||
icon=':/projector/projector_disconnect_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Disconnect from selected projector'),
|
||||
'Disconnect from selected projectors.'),
|
||||
triggers=self.on_disconnect_projector)
|
||||
self.one_toolbar.addSeparator()
|
||||
self.one_toolbar.add_toolbar_action('poweron_projector',
|
||||
@ -144,26 +144,26 @@ class Ui_ProjectorManager(object):
|
||||
'Power on selected projector'),
|
||||
icon=':/projector/projector_power_on.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Power on selected projector'),
|
||||
'Power on selected projector.'),
|
||||
triggers=self.on_poweron_projector)
|
||||
self.one_toolbar.add_toolbar_action('poweron_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Power on selected projector'),
|
||||
icon=':/projector/projector_power_on_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Power on selected projector'),
|
||||
'Power on selected projectors.'),
|
||||
triggers=self.on_poweron_projector)
|
||||
self.one_toolbar.add_toolbar_action('poweroff_projector',
|
||||
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
|
||||
icon=':/projector/projector_power_off.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Put selected projector in standby'),
|
||||
'Put selected projector in standby.'),
|
||||
triggers=self.on_poweroff_projector)
|
||||
self.one_toolbar.add_toolbar_action('poweroff_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
|
||||
icon=':/projector/projector_power_off_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Put selected projector in standby'),
|
||||
'Put selected projectors in standby.'),
|
||||
triggers=self.on_poweroff_projector)
|
||||
self.one_toolbar.addSeparator()
|
||||
self.one_toolbar.add_toolbar_action('blank_projector',
|
||||
@ -175,24 +175,24 @@ class Ui_ProjectorManager(object):
|
||||
triggers=self.on_blank_projector)
|
||||
self.one_toolbar.add_toolbar_action('blank_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Blank selected projector screen'),
|
||||
'Blank selected projectors screen'),
|
||||
icon=':/projector/projector_blank_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Blank selected projector screen'),
|
||||
'Blank selected projectors screen.'),
|
||||
triggers=self.on_blank_projector)
|
||||
self.one_toolbar.add_toolbar_action('show_projector',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Show selected projector screen'),
|
||||
icon=':/projector/projector_show.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Show selected projector screen'),
|
||||
'Show selected projector screen.'),
|
||||
triggers=self.on_show_projector)
|
||||
self.one_toolbar.add_toolbar_action('show_projector_multiple',
|
||||
text=translate('OpenLP.ProjectorManager',
|
||||
'Show selected projector screen'),
|
||||
icon=':/projector/projector_show_tiled.png',
|
||||
tooltip=translate('OpenLP.ProjectorManager',
|
||||
'Show selected projector screen'),
|
||||
'Show selected projectors screen.'),
|
||||
triggers=self.on_show_projector)
|
||||
self.layout.addWidget(self.one_toolbar)
|
||||
self.projector_one_widget = QtWidgets.QWidgetAction(self.one_toolbar)
|
||||
|
@ -23,23 +23,22 @@
|
||||
The service manager sets up, loads, saves and manages services.
|
||||
"""
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
import json
|
||||
from tempfile import mkstemp
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import mkstemp
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \
|
||||
RegistryMixin, check_directory_exists, UiStrings, translate
|
||||
RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.lib import OpenLPToolbar, ServiceItem, ItemCapabilities, PluginStatus, build_icon
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box
|
||||
from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm
|
||||
from openlp.core.ui.printserviceform import PrintServiceForm
|
||||
from openlp.core.utils import delete_file, split_filename, format_time
|
||||
from openlp.core.utils.actions import ActionList, CategoryOrder
|
||||
from openlp.core.common.languagemanager import format_time
|
||||
|
||||
|
||||
class ServiceManagerList(QtWidgets.QTreeWidget):
|
||||
@ -211,7 +210,8 @@ class Ui_ServiceManager(object):
|
||||
self.layout.addWidget(self.order_toolbar)
|
||||
# Connect up our signals and slots
|
||||
self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected)
|
||||
self.service_manager_list.doubleClicked.connect(self.on_make_live)
|
||||
self.service_manager_list.doubleClicked.connect(self.on_double_click_live)
|
||||
self.service_manager_list.clicked.connect(self.on_single_click_preview)
|
||||
self.service_manager_list.itemCollapsed.connect(self.collapsed)
|
||||
self.service_manager_list.itemExpanded.connect(self.expanded)
|
||||
# Last little bits of setting up
|
||||
@ -319,6 +319,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
self._modified = False
|
||||
self._file_name = ''
|
||||
self.service_has_all_original_files = True
|
||||
self.list_double_clicked = False
|
||||
|
||||
def bootstrap_initialise(self):
|
||||
"""
|
||||
@ -1454,13 +1455,38 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
else:
|
||||
return self.service_items[item]['service_item']
|
||||
|
||||
def on_make_live(self, field=None):
|
||||
def on_double_click_live(self, field=None):
|
||||
"""
|
||||
Send the current item to the Live slide controller but triggered by a tablewidget click event.
|
||||
:param field:
|
||||
"""
|
||||
self.list_double_clicked = True
|
||||
self.make_live()
|
||||
|
||||
def on_single_click_preview(self, field=None):
|
||||
"""
|
||||
If single click previewing is enabled, and triggered by a tablewidget click event,
|
||||
start a timeout to verify a double-click hasn't triggered.
|
||||
:param field:
|
||||
"""
|
||||
if Settings().value('advanced/single click service preview'):
|
||||
if not self.list_double_clicked:
|
||||
# If a double click has not registered start a timer, otherwise wait for the existing timer to finish.
|
||||
QtCore.QTimer.singleShot(QtWidgets.QApplication.instance().doubleClickInterval(),
|
||||
self.on_single_click_preview_timeout)
|
||||
|
||||
def on_single_click_preview_timeout(self):
|
||||
"""
|
||||
If a single click ok, but double click not triggered, send the current item to the Preview slide controller.
|
||||
:param field:
|
||||
"""
|
||||
if self.list_double_clicked:
|
||||
# If a double click has registered, clear it.
|
||||
self.list_double_clicked = False
|
||||
else:
|
||||
# Otherwise preview the item.
|
||||
self.make_preview()
|
||||
|
||||
def make_live(self, row=-1):
|
||||
"""
|
||||
Send the current item to the Live slide controller
|
||||
|
@ -27,7 +27,7 @@ import re
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import RegistryProperties, Settings, translate
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.core.common.actions import ActionList
|
||||
from .shortcutlistdialog import Ui_ShortcutListDialog
|
||||
|
||||
REMOVE_AMPERSAND = re.compile(r'&{1}')
|
||||
|
@ -23,20 +23,20 @@
|
||||
The :mod:`slidecontroller` module contains the most important part of OpenLP - the slide controller
|
||||
"""
|
||||
|
||||
import os
|
||||
import copy
|
||||
import os
|
||||
from collections import deque
|
||||
from threading import Lock
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, Settings, SlideLimits, UiStrings, translate, \
|
||||
RegistryMixin, OpenLPMixin, is_win
|
||||
RegistryMixin, OpenLPMixin
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageSource, ServiceItemAction, \
|
||||
ScreenList, build_icon, build_html
|
||||
from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.utils.actions import ActionList, CategoryOrder
|
||||
from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
|
||||
from openlp.core.ui.listpreviewwidget import ListPreviewWidget
|
||||
|
||||
# Threshold which has to be trespassed to toggle.
|
||||
@ -601,13 +601,21 @@ class SlideController(DisplayController, RegistryProperties):
|
||||
def __add_actions_to_widget(self, widget):
|
||||
"""
|
||||
Add actions to the widget specified by `widget`
|
||||
This defines the controls available when Live display has stolen focus.
|
||||
Examples of this happening: Clicking anything in the live window or certain single screen mode scenarios.
|
||||
Needles to say, blank to modes should not be removed from here.
|
||||
For some reason this required a test. It may be found in test_slidecontroller.py as
|
||||
"live_stolen_focus_shortcuts_test. If you want to modify things here, you must also modify them there. (Duh)
|
||||
|
||||
:param widget: The UI widget for the actions
|
||||
"""
|
||||
widget.addActions([
|
||||
self.previous_item, self.next_item,
|
||||
self.previous_service, self.next_service,
|
||||
self.escape_item])
|
||||
self.escape_item,
|
||||
self.desktop_screen,
|
||||
self.theme_screen,
|
||||
self.blank_screen])
|
||||
|
||||
def preview_size_changed(self):
|
||||
"""
|
||||
|
@ -27,11 +27,10 @@ import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, UiStrings, translate
|
||||
from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
|
||||
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui import ThemeLayoutForm
|
||||
from openlp.core.utils import get_images_filter, is_not_image_file
|
||||
from .themewizard import Ui_ThemeWizard
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -30,13 +30,13 @@ from xml.etree.ElementTree import ElementTree, XML
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
|
||||
check_directory_exists, UiStrings, translate, is_win
|
||||
check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
|
||||
from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationError, get_text_file_string, build_icon, \
|
||||
check_item_selected, create_thumb, validate_thumb
|
||||
from openlp.core.lib.theme import ThemeXML, BackgroundType
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
|
||||
from openlp.core.ui import FileRenameForm, ThemeForm
|
||||
from openlp.core.utils import delete_file, get_locale_key, get_filesystem_encoding
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
|
||||
|
||||
class Ui_ThemeManager(object):
|
||||
|
@ -1,543 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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.utils` module provides the utility libraries for OpenLP.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from distutils.version import LooseVersion
|
||||
from http.client import HTTPException
|
||||
import logging
|
||||
import locale
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
from shutil import which
|
||||
from subprocess import Popen, PIPE
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from random import randint
|
||||
|
||||
from PyQt5 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Registry, AppLocation, Settings, is_win, is_macosx
|
||||
|
||||
|
||||
if not is_win() and not is_macosx():
|
||||
try:
|
||||
from xdg import BaseDirectory
|
||||
XDG_BASE_AVAILABLE = True
|
||||
except ImportError:
|
||||
BaseDirectory = None
|
||||
XDG_BASE_AVAILABLE = False
|
||||
|
||||
from openlp.core.common import translate
|
||||
|
||||
log = logging.getLogger(__name__ + '.__init__')
|
||||
|
||||
APPLICATION_VERSION = {}
|
||||
IMAGES_FILTER = None
|
||||
ICU_COLLATOR = None
|
||||
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
|
||||
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
|
||||
DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
|
||||
USER_AGENTS = {
|
||||
'win32': [
|
||||
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36'
|
||||
],
|
||||
'darwin': [
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) '
|
||||
'Chrome/26.0.1410.43 Safari/537.31',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) '
|
||||
'Chrome/20.0.1132.57 Safari/536.11',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) '
|
||||
'Chrome/20.0.1132.47 Safari/536.11',
|
||||
],
|
||||
'linux2': [
|
||||
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 '
|
||||
'Chrome/25.0.1364.160 Safari/537.22',
|
||||
'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 '
|
||||
'Safari/537.11',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27'
|
||||
],
|
||||
'default': [
|
||||
'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0'
|
||||
]
|
||||
}
|
||||
CONNECTION_TIMEOUT = 30
|
||||
CONNECTION_RETRIES = 2
|
||||
|
||||
|
||||
class VersionThread(QtCore.QThread):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
def __init__(self, main_window):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
|
||||
:param main_window: The main window Object.
|
||||
"""
|
||||
log.debug("VersionThread - Initialise")
|
||||
super(VersionThread, self).__init__(None)
|
||||
self.main_window = main_window
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
self.sleep(1)
|
||||
log.debug('Version thread - run')
|
||||
app_version = get_application_version()
|
||||
version = check_latest_version(app_version)
|
||||
log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full']))))
|
||||
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
|
||||
self.main_window.openlp_version_check.emit('%s' % version)
|
||||
|
||||
|
||||
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_application_version():
|
||||
"""
|
||||
Returns the application version of the running instance of OpenLP::
|
||||
|
||||
{'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
|
||||
"""
|
||||
global APPLICATION_VERSION
|
||||
if APPLICATION_VERSION:
|
||||
return APPLICATION_VERSION
|
||||
if '--dev-version' in sys.argv or '-d' in sys.argv:
|
||||
# NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
|
||||
# there.
|
||||
|
||||
# Get the revision of this tree.
|
||||
bzr = Popen(('bzr', 'revno'), stdout=PIPE)
|
||||
tree_revision, error = bzr.communicate()
|
||||
tree_revision = tree_revision.decode()
|
||||
code = bzr.wait()
|
||||
if code != 0:
|
||||
raise Exception('Error running bzr log')
|
||||
|
||||
# Get all tags.
|
||||
bzr = Popen(('bzr', 'tags'), stdout=PIPE)
|
||||
output, error = bzr.communicate()
|
||||
code = bzr.wait()
|
||||
if code != 0:
|
||||
raise Exception('Error running bzr tags')
|
||||
tags = list(map(bytes.decode, output.splitlines()))
|
||||
if not tags:
|
||||
tag_version = '0.0.0'
|
||||
tag_revision = '0'
|
||||
else:
|
||||
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
|
||||
# another series.
|
||||
tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
|
||||
# Get the last tag and split it in a revision and tag name.
|
||||
tag_version, tag_revision = tags[-1].split()
|
||||
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision
|
||||
# number in the full version.
|
||||
if tree_revision == tag_revision:
|
||||
full_version = tag_version.decode('utf-8')
|
||||
else:
|
||||
full_version = '%s-bzr%s' % (tag_version.decode('utf-8'), tree_revision.decode('utf-8'))
|
||||
else:
|
||||
# We're not running the development version, let's use the file.
|
||||
file_path = AppLocation.get_directory(AppLocation.VersionDir)
|
||||
file_path = os.path.join(file_path, '.version')
|
||||
version_file = None
|
||||
try:
|
||||
version_file = open(file_path, 'r')
|
||||
full_version = str(version_file.read()).rstrip()
|
||||
except IOError:
|
||||
log.exception('Error in version file.')
|
||||
full_version = '0.0.0-bzr000'
|
||||
finally:
|
||||
if version_file:
|
||||
version_file.close()
|
||||
bits = full_version.split('-')
|
||||
APPLICATION_VERSION = {
|
||||
'full': full_version,
|
||||
'version': bits[0],
|
||||
'build': bits[1] if len(bits) > 1 else None
|
||||
}
|
||||
if APPLICATION_VERSION['build']:
|
||||
log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build'])
|
||||
else:
|
||||
log.info('Openlp version %s' % 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/%s %s/%s; ' % (current_version['full'], platform.system(),
|
||||
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
|
||||
|
||||
|
||||
def add_actions(target, actions):
|
||||
"""
|
||||
Adds multiple actions to a menu or toolbar in one command.
|
||||
|
||||
:param target: The menu or toolbar to add actions to
|
||||
:param actions: The actions to be added. An action consisting of the keyword ``None``
|
||||
will result in a separator being inserted into the target.
|
||||
"""
|
||||
for action in actions:
|
||||
if action is None:
|
||||
target.addSeparator()
|
||||
else:
|
||||
target.addAction(action)
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
"""
|
||||
Returns the name of the encoding used to convert Unicode filenames into system file names.
|
||||
"""
|
||||
encoding = sys.getfilesystemencoding()
|
||||
if encoding is None:
|
||||
encoding = sys.getdefaultencoding()
|
||||
return encoding
|
||||
|
||||
|
||||
def get_images_filter():
|
||||
"""
|
||||
Returns a filter string for a file dialog containing all the supported image formats.
|
||||
"""
|
||||
global IMAGES_FILTER
|
||||
if not IMAGES_FILTER:
|
||||
log.debug('Generating images filter.')
|
||||
formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
|
||||
visible_formats = '(*.%s)' % '; *.'.join(formats)
|
||||
actual_formats = '(*.%s)' % ' *.'.join(formats)
|
||||
IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
|
||||
return IMAGES_FILTER
|
||||
|
||||
|
||||
def is_not_image_file(file_name):
|
||||
"""
|
||||
Validate that the file is not an image file.
|
||||
|
||||
:param file_name: File name to be checked.
|
||||
"""
|
||||
if not file_name:
|
||||
return True
|
||||
else:
|
||||
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
|
||||
file_part, file_extension = os.path.splitext(str(file_name))
|
||||
if file_extension[1:].lower() in formats and os.path.exists(file_name):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def split_filename(path):
|
||||
"""
|
||||
Return a list of the parts in a given path.
|
||||
"""
|
||||
path = os.path.abspath(path)
|
||||
if not os.path.isfile(path):
|
||||
return path, ''
|
||||
else:
|
||||
return os.path.split(path)
|
||||
|
||||
|
||||
def clean_filename(filename):
|
||||
"""
|
||||
Removes invalid characters from the given ``filename``.
|
||||
|
||||
:param filename: The "dirty" file name to clean.
|
||||
"""
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, 'utf-8')
|
||||
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
|
||||
|
||||
|
||||
def delete_file(file_path_name):
|
||||
"""
|
||||
Deletes a file from the system.
|
||||
|
||||
:param file_path_name: The file, including path, to delete.
|
||||
"""
|
||||
if not file_path_name:
|
||||
return False
|
||||
try:
|
||||
if os.path.exists(file_path_name):
|
||||
os.remove(file_path_name)
|
||||
return True
|
||||
except (IOError, OSError):
|
||||
log.exception("Unable to delete file %s" % file_path_name)
|
||||
return False
|
||||
|
||||
|
||||
def _get_user_agent():
|
||||
"""
|
||||
Return a user agent customised for the platform the user is on.
|
||||
"""
|
||||
browser_list = USER_AGENTS.get(sys.platform, None)
|
||||
if not browser_list:
|
||||
browser_list = USER_AGENTS['default']
|
||||
random_index = randint(0, len(browser_list) - 1)
|
||||
return browser_list[random_index]
|
||||
|
||||
|
||||
def get_web_page(url, header=None, update_openlp=False):
|
||||
"""
|
||||
Attempts to download the webpage at url and returns that page or None.
|
||||
|
||||
:param url: The URL to be downloaded.
|
||||
:param header: An optional HTTP header to pass in the request to the web server.
|
||||
: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])
|
||||
log.debug('Downloading URL = %s' % url)
|
||||
retries = 0
|
||||
while retries <= CONNECTION_RETRIES:
|
||||
retries += 1
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
|
||||
log.debug('Downloaded page {}'.format(page.geturl()))
|
||||
break
|
||||
except urllib.error.URLError as err:
|
||||
log.exception('URLError on {}'.format(url))
|
||||
log.exception('URLError: {}'.format(err.reason))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except socket.timeout:
|
||||
log.exception('Socket timeout: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except socket.gaierror:
|
||||
log.exception('Socket gaierror: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except ConnectionRefusedError:
|
||||
log.exception('ConnectionRefused: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
break
|
||||
except ConnectionError:
|
||||
log.exception('Connection error: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except HTTPException:
|
||||
log.exception('HTTPException error: {}'.format(url))
|
||||
page = None
|
||||
if retries > CONNECTION_RETRIES:
|
||||
raise
|
||||
except:
|
||||
# Don't know what's happening, so reraise the original
|
||||
raise
|
||||
if update_openlp:
|
||||
Registry().get('application').process_events()
|
||||
if not page:
|
||||
log.exception('{} could not be downloaded'.format(url))
|
||||
return None
|
||||
log.debug(page)
|
||||
return page
|
||||
|
||||
|
||||
def get_uno_command(connection_type='pipe'):
|
||||
"""
|
||||
Returns the UNO command to launch an openoffice.org instance.
|
||||
"""
|
||||
for command in ['libreoffice', 'soffice']:
|
||||
if which(command):
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError('Command not found')
|
||||
|
||||
OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
if connection_type == 'pipe':
|
||||
CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
|
||||
else:
|
||||
CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
|
||||
return '%s %s %s' % (command, OPTIONS, CONNECTION)
|
||||
|
||||
|
||||
def get_uno_instance(resolver, connection_type='pipe'):
|
||||
"""
|
||||
Returns a running openoffice.org instance.
|
||||
|
||||
:param resolver: The UNO resolver to use to find a running instance.
|
||||
"""
|
||||
log.debug('get UNO Desktop Openoffice - resolve')
|
||||
if connection_type == 'pipe':
|
||||
return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
|
||||
else:
|
||||
return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
|
||||
|
||||
|
||||
def format_time(text, local_time):
|
||||
"""
|
||||
Workaround for Python built-in time formatting function time.strftime().
|
||||
|
||||
time.strftime() accepts only ascii characters. This function accepts
|
||||
unicode string and passes individual % placeholders to time.strftime().
|
||||
This ensures only ascii characters are passed to time.strftime().
|
||||
|
||||
:param text: The text to be processed.
|
||||
:param local_time: The time to be used to add to the string. This is a time object
|
||||
"""
|
||||
def match_formatting(match):
|
||||
"""
|
||||
Format the match
|
||||
"""
|
||||
return local_time.strftime(match.group())
|
||||
return re.sub('\%[a-zA-Z]', match_formatting, text)
|
||||
|
||||
|
||||
def get_locale_key(string):
|
||||
"""
|
||||
Creates a key for case insensitive, locale aware string sorting.
|
||||
|
||||
:param string: The corresponding string.
|
||||
"""
|
||||
string = string.lower()
|
||||
# ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
|
||||
global ICU_COLLATOR
|
||||
try:
|
||||
if ICU_COLLATOR is None:
|
||||
import icu
|
||||
from .languagemanager import LanguageManager
|
||||
language = LanguageManager.get_language()
|
||||
icu_locale = icu.Locale(language)
|
||||
ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
|
||||
return ICU_COLLATOR.getSortKey(string)
|
||||
except:
|
||||
return locale.strxfrm(string).encode()
|
||||
|
||||
|
||||
def get_natural_key(string):
|
||||
"""
|
||||
Generate a key for locale aware natural string sorting.
|
||||
Returns a list of string compare keys and integers.
|
||||
"""
|
||||
key = DIGITS_OR_NONDIGITS.findall(string)
|
||||
key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
|
||||
# Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
|
||||
# and int.
|
||||
if string[0].isdigit():
|
||||
return [b''] + key
|
||||
return key
|
||||
|
||||
|
||||
from .languagemanager import LanguageManager
|
||||
from .actions import ActionList
|
||||
|
||||
|
||||
__all__ = ['ActionList', 'LanguageManager', 'get_application_version', 'check_latest_version',
|
||||
'add_actions', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance',
|
||||
'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key']
|
@ -24,17 +24,16 @@ import logging
|
||||
|
||||
from PyQt5 import QtGui
|
||||
|
||||
|
||||
from openlp.core.common import Settings, translate
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.ui import create_action, UiStrings
|
||||
from openlp.core.lib.theme import VerticalType
|
||||
from openlp.core.lib.ui import create_action, UiStrings
|
||||
from openlp.core.ui import AlertLocation
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.plugins.alerts.forms import AlertForm
|
||||
from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
|
||||
from openlp.plugins.alerts.lib.db import init_schema
|
||||
from openlp.plugins.alerts.forms import AlertForm
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,13 +24,13 @@ import logging
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon, translate
|
||||
from openlp.core.lib.ui import UiStrings, create_action
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.plugins.bibles.forms import BibleUpgradeForm
|
||||
from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \
|
||||
LanguageSelection
|
||||
from openlp.plugins.bibles.lib.mediaitem import BibleSearch
|
||||
from openlp.plugins.bibles.forms import BibleUpgradeForm
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -28,11 +28,11 @@ import urllib.error
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, UiStrings, translate
|
||||
from openlp.core.common import AppLocation, Settings, UiStrings, translate, clean_filename
|
||||
from openlp.core.lib.db import delete_database
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.plugins.bibles.lib.manager import BibleFormat
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename
|
||||
from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract
|
||||
|
@ -29,10 +29,10 @@ from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, AppLocation, UiStrings, Settings, check_directory_exists, translate
|
||||
from openlp.core.common import Registry, AppLocation, UiStrings, Settings, check_directory_exists, translate, \
|
||||
delete_file
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.core.utils import delete_file
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta, OldBibleDB, BiblesResourcesDB
|
||||
from openlp.plugins.bibles.lib.http import BSExtract, BGExtract, CWExtract
|
||||
|
||||
|
@ -33,10 +33,9 @@ from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.orm import class_mapper, mapper, relation
|
||||
from sqlalchemy.orm.exc import UnmappedClassError
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, translate
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, translate, clean_filename
|
||||
from openlp.core.lib.db import BaseModel, init_db, Manager
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.utils import clean_filename
|
||||
from openlp.plugins.bibles.lib import upgrade
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -32,7 +32,7 @@ from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, translate
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.utils import get_web_page
|
||||
from openlp.core.lib.webpagereader import get_web_page
|
||||
from openlp.plugins.bibles.lib import SearchResults
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book
|
||||
|
||||
|
@ -23,8 +23,7 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from openlp.core.common import RegistryProperties, AppLocation, Settings, translate
|
||||
from openlp.core.utils import delete_file
|
||||
from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file
|
||||
from openlp.plugins.bibles.lib import parse_reference, get_reference_separator, LanguageSelection
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
|
||||
from .csvbible import CSVBible
|
||||
|
@ -29,7 +29,7 @@ from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemConte
|
||||
from openlp.core.lib.searchedit import SearchEdit
|
||||
from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \
|
||||
critical_error_message_box, find_and_set_in_combo_box, build_icon
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm
|
||||
from openlp.plugins.bibles.forms.editbibleform import EditBibleForm
|
||||
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
|
||||
|
@ -198,6 +198,7 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog):
|
||||
# Insert all slides to make the old_slides list complete.
|
||||
for slide in slides:
|
||||
old_slides.insert(old_row, slide)
|
||||
old_row += 1
|
||||
self.slide_list_view.addItems(old_slides)
|
||||
self.slide_list_view.repaint()
|
||||
|
||||
|
@ -28,7 +28,7 @@ from sqlalchemy import Column, Table, types
|
||||
from sqlalchemy.orm import mapper
|
||||
|
||||
from openlp.core.lib.db import BaseModel, init_db
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
|
||||
|
||||
class CustomSlide(BaseModel):
|
||||
|
@ -25,11 +25,12 @@ import os
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate
|
||||
from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \
|
||||
delete_file, get_images_filter
|
||||
from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\
|
||||
build_icon, check_item_selected, create_thumb, validate_thumb
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
|
||||
from openlp.core.utils import delete_file, get_locale_key, get_images_filter
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm
|
||||
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
|
||||
|
||||
|
@ -32,7 +32,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box, create_horizontal_adjusting_combo_box
|
||||
from openlp.core.ui import DisplayControllerType
|
||||
from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.core.ui.media.vlcplayer import get_vlc
|
||||
|
||||
if get_vlc() is not None:
|
||||
|
@ -35,7 +35,7 @@ import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from openlp.core.common import is_win, Registry
|
||||
from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file
|
||||
|
||||
if is_win():
|
||||
from win32com.client import Dispatch
|
||||
@ -57,7 +57,7 @@ else:
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.lib import ScreenList
|
||||
from openlp.core.utils import delete_file, get_uno_command, get_uno_instance
|
||||
from openlp.core.common import get_uno_command, get_uno_instance
|
||||
from .presentationcontroller import PresentationController, PresentationDocument, TextType
|
||||
|
||||
|
||||
|
@ -29,7 +29,7 @@ from openlp.core.common import Registry, Settings, UiStrings, translate
|
||||
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
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.plugins.presentations.lib import MessageListener
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
||||
|
||||
|
@ -27,7 +27,7 @@ import re
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
|
||||
from openlp.core.utils import AppLocation
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.common import Settings, is_win, trace_error_handler
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
|
@ -20,7 +20,6 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
import zipfile
|
||||
@ -34,7 +33,7 @@ if is_win():
|
||||
from ctypes import cdll
|
||||
from ctypes.wintypes import RECT
|
||||
|
||||
from openlp.core.utils import AppLocation
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.lib import ScreenList
|
||||
from .presentationcontroller import PresentationController, PresentationDocument
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
(function ( root, doc, factory ) {
|
||||
if ( typeof define === "function" && define.amd ) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define( [ "jquery" ], function ( $ ) {
|
||||
define( [ "jquery" ], function ($ ) {
|
||||
factory( $, root, doc );
|
||||
return $.mobile;
|
||||
});
|
2
openlp/plugins/remotes/html/assets/jquery.mobile.min.css
vendored
Normal file
2
openlp/plugins/remotes/html/assets/jquery.mobile.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
openlp/plugins/remotes/html/assets/jquery.mobile.min.js
vendored
Normal file
2
openlp/plugins/remotes/html/assets/jquery.mobile.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -18,11 +18,11 @@
|
||||
******************************************************************************/
|
||||
|
||||
.ui-icon-blank {
|
||||
background-image: url(images/ui-icon-blank.png);
|
||||
background-image: url(../images/ui-icon-blank.png);
|
||||
}
|
||||
|
||||
.ui-icon-unblank {
|
||||
background-image: url(images/ui-icon-unblank.png);
|
||||
background-image: url(../images/ui-icon-unblank.png);
|
||||
}
|
||||
|
||||
/* Overwrite style from jquery-mobile.min.css */
|
@ -24,12 +24,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1" />
|
||||
<title>${app_title}</title>
|
||||
<link rel="stylesheet" href="/files/jquery.mobile.min.css" />
|
||||
<link rel="stylesheet" href="/files/openlp.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
|
||||
<script type="text/javascript" src="/files/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/files/openlp.js"></script>
|
||||
<script type="text/javascript" src="/files/jquery.mobile.min.js"></script>
|
||||
<link rel="stylesheet" href="/assets/jquery.mobile.min.css" />
|
||||
<link rel="stylesheet" href="/css/openlp.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/openlp.js"></script>
|
||||
<script type="text/javascript" src="/assets/jquery.mobile.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
translationStrings = {
|
||||
"go_live": "${go_live}",
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -23,10 +23,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${live_title}</title>
|
||||
<link rel="stylesheet" href="/files/main.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
|
||||
<script type="text/javascript" src="/files/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/files/main.js"></script>
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="image" class="size"/>
|
||||
|
@ -23,10 +23,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${stage_title}</title>
|
||||
<link rel="stylesheet" href="/files/stage.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
|
||||
<script type="text/javascript" src="/files/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/files/stage.js"></script>
|
||||
<link rel="stylesheet" href="/css/stage.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/stage.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="hidden" id="next-text" value="${next}" />
|
||||
|
@ -146,12 +146,12 @@ class HttpRouter(RegistryProperties):
|
||||
self.auth = base64.b64encode(auth_code)
|
||||
except TypeError:
|
||||
self.auth = base64.b64encode(auth_code.encode()).decode()
|
||||
self.default_route = {'function': self.serve_file, 'secure': False}
|
||||
self.routes = [
|
||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||
(r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
|
||||
@ -221,6 +221,7 @@ class HttpRouter(RegistryProperties):
|
||||
self.request_data = None
|
||||
url_path_split = urlparse(url_path)
|
||||
url_query = parse_qs(url_path_split.query)
|
||||
# GET
|
||||
if 'data' in url_query.keys():
|
||||
self.request_data = url_query['data'][0]
|
||||
for route, func in self.routes:
|
||||
@ -231,7 +232,7 @@ class HttpRouter(RegistryProperties):
|
||||
for param in match.groups():
|
||||
args.append(param)
|
||||
return func, args
|
||||
return None, None
|
||||
return self.default_route, [url_path_split.path]
|
||||
|
||||
def set_cache_headers(self):
|
||||
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
@ -404,6 +405,8 @@ class HttpRouter(RegistryProperties):
|
||||
file_name = 'stage.html'
|
||||
elif file_name == 'main':
|
||||
file_name = 'main.html'
|
||||
if file_name.startswith('/'):
|
||||
file_name = file_name[1:]
|
||||
path = os.path.normpath(os.path.join(self.html_dir, file_name))
|
||||
if not path.startswith(self.html_dir):
|
||||
return self.do_not_found()
|
||||
|
@ -25,7 +25,7 @@ The :mod:`~openlp.plugins.songs.forms.songselectdialog` module contains the user
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import HistoryComboBox
|
||||
from openlp.core.ui.lib.historycombobox import HistoryComboBox
|
||||
from openlp.core.lib import translate, build_icon
|
||||
from openlp.core.ui import SingleColumnTableWidget
|
||||
|
||||
|
@ -29,9 +29,8 @@ import re
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.common import AppLocation, CONTROL_CHARS
|
||||
from openlp.core.lib import translate
|
||||
from openlp.core.utils import CONTROL_CHARS
|
||||
from openlp.plugins.songs.lib.db import MediaFile, Song
|
||||
from .db import Author
|
||||
from .ui import SongStrings
|
||||
|
@ -29,7 +29,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor
|
||||
from sqlalchemy.sql.expression import func, text
|
||||
|
||||
from openlp.core.lib.db import BaseModel, init_db
|
||||
from openlp.core.utils import get_natural_key
|
||||
from openlp.core.common.languagemanager import get_natural_key
|
||||
from openlp.core.lib import translate
|
||||
|
||||
|
||||
|
@ -78,6 +78,13 @@ if is_win():
|
||||
HAS_WORSHIPCENTERPRO = True
|
||||
except ImportError:
|
||||
log.exception('Error importing %s', 'WorshipCenterProImport')
|
||||
HAS_OPSPRO = False
|
||||
if is_win():
|
||||
try:
|
||||
from .importers.opspro import OPSProImport
|
||||
HAS_OPSPRO = True
|
||||
except ImportError:
|
||||
log.exception('Error importing %s', 'OPSProImport')
|
||||
|
||||
|
||||
class SongFormatSelect(object):
|
||||
@ -156,20 +163,21 @@ class SongFormat(object):
|
||||
Lyrix = 9
|
||||
MediaShout = 10
|
||||
OpenSong = 11
|
||||
PowerPraise = 12
|
||||
PowerSong = 13
|
||||
PresentationManager = 14
|
||||
ProPresenter = 15
|
||||
SongBeamer = 16
|
||||
SongPro = 17
|
||||
SongShowPlus = 18
|
||||
SongsOfFellowship = 19
|
||||
SundayPlus = 20
|
||||
VideoPsalm = 21
|
||||
WordsOfWorship = 22
|
||||
WorshipAssistant = 23
|
||||
WorshipCenterPro = 24
|
||||
ZionWorx = 25
|
||||
OPSPro = 12
|
||||
PowerPraise = 13
|
||||
PowerSong = 14
|
||||
PresentationManager = 15
|
||||
ProPresenter = 16
|
||||
SongBeamer = 17
|
||||
SongPro = 18
|
||||
SongShowPlus = 19
|
||||
SongsOfFellowship = 20
|
||||
SundayPlus = 21
|
||||
VideoPsalm = 22
|
||||
WordsOfWorship = 23
|
||||
WorshipAssistant = 24
|
||||
WorshipCenterPro = 25
|
||||
ZionWorx = 26
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
@ -272,6 +280,17 @@ class SongFormat(object):
|
||||
'name': WizardStrings.OS,
|
||||
'prefix': 'openSong'
|
||||
},
|
||||
OPSPro: {
|
||||
'name': 'OPS Pro',
|
||||
'prefix': 'OPSPro',
|
||||
'canDisable': True,
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'OPS Pro database'),
|
||||
'disabledLabelText': translate('SongsPlugin.ImportWizardForm',
|
||||
'The OPS Pro importer is only supported on Windows. It has been '
|
||||
'disabled due to a missing Python module. If you want to use this '
|
||||
'importer, you will need to install the "pyodbc" module.')
|
||||
},
|
||||
PowerPraise: {
|
||||
'class': PowerPraiseImport,
|
||||
'name': 'PowerPraise',
|
||||
@ -294,9 +313,9 @@ class SongFormat(object):
|
||||
},
|
||||
ProPresenter: {
|
||||
'class': ProPresenterImport,
|
||||
'name': 'ProPresenter 4',
|
||||
'name': 'ProPresenter 4, 5 and 6',
|
||||
'prefix': 'proPresenter',
|
||||
'filter': '%s (*.pro4)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter 4 Song Files')
|
||||
'filter': '%s (*.pro4 *.pro5 *.pro6)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter Song Files')
|
||||
},
|
||||
SongBeamer: {
|
||||
'class': SongBeamerImport,
|
||||
@ -403,6 +422,7 @@ class SongFormat(object):
|
||||
SongFormat.Lyrix,
|
||||
SongFormat.MediaShout,
|
||||
SongFormat.OpenSong,
|
||||
SongFormat.OPSPro,
|
||||
SongFormat.PowerPraise,
|
||||
SongFormat.PowerSong,
|
||||
SongFormat.PresentationManager,
|
||||
@ -416,7 +436,7 @@ class SongFormat(object):
|
||||
SongFormat.WordsOfWorship,
|
||||
SongFormat.WorshipAssistant,
|
||||
SongFormat.WorshipCenterPro,
|
||||
SongFormat.ZionWorx,
|
||||
SongFormat.ZionWorx
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
@ -465,6 +485,9 @@ if HAS_MEDIASHOUT:
|
||||
SongFormat.set(SongFormat.WorshipCenterPro, 'availability', HAS_WORSHIPCENTERPRO)
|
||||
if HAS_WORSHIPCENTERPRO:
|
||||
SongFormat.set(SongFormat.WorshipCenterPro, 'class', WorshipCenterProImport)
|
||||
SongFormat.set(SongFormat.OPSPro, 'availability', HAS_OPSPRO)
|
||||
if HAS_OPSPRO:
|
||||
SongFormat.set(SongFormat.OPSPro, 'class', OPSProImport)
|
||||
|
||||
|
||||
__all__ = ['SongFormat', 'SongFormatSelect']
|
||||
|
@ -289,6 +289,7 @@ class EasyWorshipSongImport(SongImport):
|
||||
for i in range(rec_count):
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
try:
|
||||
raw_record = db_file.read(record_size)
|
||||
self.fields = self.record_structure.unpack(raw_record)
|
||||
self.set_defaults()
|
||||
@ -323,6 +324,10 @@ class EasyWorshipSongImport(SongImport):
|
||||
self.entry_error_log = ''
|
||||
elif not self.finish():
|
||||
self.log_error(self.import_source)
|
||||
except Exception as e:
|
||||
self.log_error(self.import_source,
|
||||
translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
|
||||
% (self.title, e))
|
||||
db_file.close()
|
||||
self.memo_file.close()
|
||||
|
||||
@ -368,7 +373,7 @@ class EasyWorshipSongImport(SongImport):
|
||||
first_line_is_tag = False
|
||||
# EW tags: verse, chorus, pre-chorus, bridge, tag,
|
||||
# intro, ending, slide
|
||||
for tag in VerseType.tags + ['tag', 'slide']:
|
||||
for tag in VerseType.names + ['tag', 'slide', 'end']:
|
||||
tag = tag.lower()
|
||||
ew_tag = verse_split[0].strip().lower()
|
||||
if ew_tag.startswith(tag):
|
||||
@ -390,6 +395,9 @@ class EasyWorshipSongImport(SongImport):
|
||||
if not number_found:
|
||||
verse_type += '1'
|
||||
break
|
||||
# If the verse only consist of the tag-line, add an empty line to create an empty slide
|
||||
if first_line_is_tag and len(verse_split) == 1:
|
||||
verse_split.append("")
|
||||
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
|
||||
if len(self.comments) > 5:
|
||||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||
|
@ -25,8 +25,7 @@ import time
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import is_win
|
||||
from openlp.core.utils import get_uno_command, get_uno_instance
|
||||
from openlp.core.common import is_win, get_uno_command, get_uno_instance
|
||||
from openlp.core.lib import translate
|
||||
from .songimport import SongImport
|
||||
|
||||
|
260
openlp/plugins/songs/lib/importers/opspro.py
Normal file
260
openlp/plugins/songs/lib/importers/opspro.py
Normal file
@ -0,0 +1,260 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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:`opspro` module provides the functionality for importing
|
||||
a OPS Pro database into the OpenLP database.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import pyodbc
|
||||
import struct
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OPSProImport(SongImport):
|
||||
"""
|
||||
The :class:`OPSProImport` class provides the ability to import the
|
||||
WorshipCenter Pro Access Database
|
||||
"""
|
||||
def __init__(self, manager, **kwargs):
|
||||
"""
|
||||
Initialise the WorshipCenter Pro importer.
|
||||
"""
|
||||
super(OPSProImport, self).__init__(manager, **kwargs)
|
||||
|
||||
def do_import(self):
|
||||
"""
|
||||
Receive a single file to import.
|
||||
"""
|
||||
password = self.extract_mdb_password()
|
||||
try:
|
||||
conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s;PWD=%s' % (self.import_source,
|
||||
password))
|
||||
except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e:
|
||||
log.warning('Unable to connect the OPS Pro database %s. %s', self.import_source, str(e))
|
||||
# Unfortunately no specific exception type
|
||||
self.log_error(self.import_source, translate('SongsPlugin.OPSProImport',
|
||||
'Unable to connect the OPS Pro database.'))
|
||||
return
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT Song.ID, SongNumber, SongBookName, Title, CopyrightText, Version, Origin FROM Song '
|
||||
'LEFT JOIN SongBook ON Song.SongBookID = SongBook.ID ORDER BY Title')
|
||||
songs = cursor.fetchall()
|
||||
self.import_wizard.progress_bar.setMaximum(len(songs))
|
||||
for song in songs:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
# Type means: 0=Original, 1=Projection, 2=Own
|
||||
cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = %d AND Type < 2 '
|
||||
'ORDER BY Type DESC' % song.ID)
|
||||
lyrics = cursor.fetchone()
|
||||
cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory '
|
||||
'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = %d '
|
||||
'ORDER BY CategoryName' % song.ID)
|
||||
topics = cursor.fetchall()
|
||||
try:
|
||||
self.process_song(song, lyrics, topics)
|
||||
except Exception as e:
|
||||
self.log_error(self.import_source,
|
||||
translate('SongsPlugin.OPSProImport', '"%s" could not be imported. %s')
|
||||
% (song.Title, e))
|
||||
|
||||
def process_song(self, song, lyrics, topics):
|
||||
"""
|
||||
Create the song, i.e. title, verse etc.
|
||||
|
||||
The OPS Pro format is a fairly simple text format using tags and anchors/labels. Linebreaks are \r\n.
|
||||
Double linebreaks are slide dividers. OPS Pro support dual language using tags.
|
||||
Tags are in [], see the liste below:
|
||||
[join] are used to separate verses that should be keept on the same slide.
|
||||
[split] or [splits] can be used to split a verse over several slides, while still being the same verse
|
||||
Dual language tags:
|
||||
[trans off] or [vertaal uit] turns dual language mode off for the following text
|
||||
[trans on] or [vertaal aan] turns dual language mode on for the following text
|
||||
[taal a] means the following lines are language a
|
||||
[taal b] means the following lines are language b
|
||||
"""
|
||||
self.set_defaults()
|
||||
self.title = song.Title
|
||||
if song.CopyrightText:
|
||||
for line in song.CopyrightText.splitlines():
|
||||
if line.startswith('©') or line.lower().startswith('copyright'):
|
||||
self.add_copyright(line)
|
||||
else:
|
||||
self.parse_author(line)
|
||||
if song.Origin:
|
||||
self.comments = song.Origin
|
||||
if song.SongBookName:
|
||||
self.song_book_name = song.SongBookName
|
||||
if song.SongNumber:
|
||||
self.song_number = song.SongNumber
|
||||
for topic in topics:
|
||||
self.topics.append(topic.CategoryName)
|
||||
# Try to split lyrics based on various rules
|
||||
if lyrics:
|
||||
lyrics_text = lyrics.Lyrics
|
||||
verses = re.split('\r\n\s*?\r\n', lyrics_text)
|
||||
verse_tag_defs = {}
|
||||
verse_tag_texts = {}
|
||||
for verse_text in verses:
|
||||
if verse_text.strip() == '':
|
||||
continue
|
||||
verse_def = 'v'
|
||||
# Detect verse number
|
||||
verse_number = re.match('^(\d+)\r\n', verse_text)
|
||||
if verse_number:
|
||||
verse_text = re.sub('^\d+\r\n', '', verse_text)
|
||||
verse_def = 'v' + verse_number.group(1)
|
||||
# Detect verse tags
|
||||
elif re.match('^.+?\:\r\n', verse_text):
|
||||
tag_match = re.match('^(.+?)\:\r\n(.*)', verse_text, flags=re.DOTALL)
|
||||
tag = tag_match.group(1).lower()
|
||||
tag = tag.split(' ')[0]
|
||||
verse_text = tag_match.group(2)
|
||||
if 'refrein' in tag or 'chorus' in tag:
|
||||
verse_def = 'c'
|
||||
elif 'bridge' in tag:
|
||||
verse_def = 'b'
|
||||
verse_tag_defs[tag] = verse_def
|
||||
verse_tag_texts[tag] = verse_text
|
||||
# Detect tag reference
|
||||
elif re.match('^\(.*?\)$', verse_text):
|
||||
tag_match = re.match('^\((.*?)\)$', verse_text)
|
||||
tag = tag_match.group(1).lower()
|
||||
if tag in verse_tag_defs:
|
||||
verse_text = verse_tag_texts[tag]
|
||||
verse_def = verse_tag_defs[tag]
|
||||
# Detect end tag
|
||||
elif re.match('^\[slot\]\r\n', verse_text, re.IGNORECASE):
|
||||
verse_def = 'e'
|
||||
verse_text = re.sub('^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE)
|
||||
# Replace the join tag with line breaks
|
||||
verse_text = verse_text.replace('[join]', '')
|
||||
# Replace the split tag with line breaks and an optional split
|
||||
verse_text = re.sub('\[splits?\]', '\r\n[---]', verse_text)
|
||||
# Handle translations
|
||||
if lyrics.IsDualLanguage:
|
||||
verse_text = self.handle_translation(verse_text)
|
||||
# Remove comments
|
||||
verse_text = re.sub('\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE)
|
||||
self.add_verse(verse_text, verse_def)
|
||||
self.finish()
|
||||
|
||||
def handle_translation(self, verse_text):
|
||||
"""
|
||||
Replace OPS Pro translation tags with a {translation} tag
|
||||
|
||||
:param verse_text: the verse text
|
||||
:return: the verse text with replaced tags
|
||||
"""
|
||||
language = None
|
||||
translation = True
|
||||
translation_verse_text = ''
|
||||
start_tag = '{translation}'
|
||||
end_tag = '{/translation}'
|
||||
verse_text_lines = verse_text.splitlines()
|
||||
idx = 0
|
||||
while idx < len(verse_text_lines):
|
||||
# Detect if translation is turned on or off
|
||||
if verse_text_lines[idx] in ['[trans off]', '[vertaal uit]']:
|
||||
translation = False
|
||||
idx += 1
|
||||
elif verse_text_lines[idx] in ['[trans on]', '[vertaal aan]']:
|
||||
translation = True
|
||||
idx += 1
|
||||
elif verse_text_lines[idx] == '[taal a]':
|
||||
language = 'a'
|
||||
idx += 1
|
||||
elif verse_text_lines[idx] == '[taal b]':
|
||||
language = 'b'
|
||||
idx += 1
|
||||
if not idx < len(verse_text_lines):
|
||||
break
|
||||
# Handle the text based on whether translation is off or on
|
||||
if language:
|
||||
if language == 'b':
|
||||
translation_verse_text += start_tag
|
||||
while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
||||
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
||||
idx += 1
|
||||
if language == 'b':
|
||||
translation_verse_text += end_tag
|
||||
language = None
|
||||
elif translation:
|
||||
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
||||
idx += 1
|
||||
if idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
||||
translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n'
|
||||
idx += 1
|
||||
else:
|
||||
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
||||
idx += 1
|
||||
while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['):
|
||||
translation_verse_text += verse_text_lines[idx] + '\r\n'
|
||||
idx += 1
|
||||
return translation_verse_text
|
||||
|
||||
def extract_mdb_password(self):
|
||||
"""
|
||||
Extract password from mdb. Based on code from
|
||||
http://tutorialsto.com/database/access/crack-access-*.-mdb-all-current-versions-of-the-password.html
|
||||
"""
|
||||
# The definition of 13 bytes as the source XOR Access2000. Encrypted with the corresponding signs are 0x13
|
||||
xor_pattern_2k = (0xa1, 0xec, 0x7a, 0x9c, 0xe1, 0x28, 0x34, 0x8a, 0x73, 0x7b, 0xd2, 0xdf, 0x50)
|
||||
# Access97 XOR of the source
|
||||
xor_pattern_97 = (0x86, 0xfb, 0xec, 0x37, 0x5d, 0x44, 0x9c, 0xfa, 0xc6, 0x5e, 0x28, 0xe6, 0x13)
|
||||
mdb = open(self.import_source, 'rb')
|
||||
mdb.seek(0x14)
|
||||
version = struct.unpack('B', mdb.read(1))[0]
|
||||
# Get encrypted logo
|
||||
mdb.seek(0x62)
|
||||
EncrypFlag = struct.unpack('B', mdb.read(1))[0]
|
||||
# Get encrypted password
|
||||
mdb.seek(0x42)
|
||||
encrypted_password = mdb.read(26)
|
||||
mdb.close()
|
||||
# "Decrypt" the password based on the version
|
||||
decrypted_password = ''
|
||||
if version < 0x01:
|
||||
# Access 97
|
||||
if int(encrypted_password[0] ^ xor_pattern_97[0]) == 0:
|
||||
# No password
|
||||
decrypted_password = ''
|
||||
else:
|
||||
for j in range(0, 12):
|
||||
decrypted_password = decrypted_password + chr(encrypted_password[j] ^ xor_pattern_97[j])
|
||||
else:
|
||||
# Access 2000 or 2002
|
||||
for j in range(0, 12):
|
||||
if j % 2 == 0:
|
||||
# Every byte with a different sign or encrypt. Encryption signs here for the 0x13
|
||||
t1 = chr(0x13 ^ EncrypFlag ^ encrypted_password[j * 2] ^ xor_pattern_2k[j])
|
||||
else:
|
||||
t1 = chr(encrypted_password[j * 2] ^ xor_pattern_2k[j])
|
||||
decrypted_password = decrypted_password + t1
|
||||
if ord(decrypted_password[1]) < 0x20 or ord(decrypted_password[1]) > 0x7e:
|
||||
decrypted_password = ''
|
||||
return decrypted_password
|
@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
|
||||
class ProPresenterImport(SongImport):
|
||||
"""
|
||||
The :class:`ProPresenterImport` class provides OpenLP with the
|
||||
ability to import ProPresenter 4 song files.
|
||||
ability to import ProPresenter 4-6 song files.
|
||||
"""
|
||||
def do_import(self):
|
||||
self.import_wizard.progress_bar.setMaximum(len(self.import_source))
|
||||
@ -52,14 +52,32 @@ class ProPresenterImport(SongImport):
|
||||
|
||||
def process_song(self, root, filename):
|
||||
self.set_defaults()
|
||||
self.title = os.path.basename(filename).rstrip('.pro4')
|
||||
self.copyright = root.get('CCLICopyrightInfo')
|
||||
|
||||
# Extract ProPresenter versionNumber
|
||||
try:
|
||||
self.version = int(root.get('versionNumber'))
|
||||
except ValueError:
|
||||
log.debug('ProPresenter versionNumber invalid or missing')
|
||||
return
|
||||
|
||||
# Title
|
||||
self.title = root.get('CCLISongTitle')
|
||||
if not self.title or self.title == '':
|
||||
self.title = os.path.basename(filename)
|
||||
if self.title[-5:-1] == '.pro':
|
||||
self.title = self.title[:-5]
|
||||
# Notes
|
||||
self.comments = root.get('notes')
|
||||
self.ccli_number = root.get('CCLILicenseNumber')
|
||||
for author_key in ['author', 'artist', 'CCLIArtistCredits']:
|
||||
# Author
|
||||
for author_key in ['author', 'CCLIAuthor', 'artist', 'CCLIArtistCredits']:
|
||||
author = root.get(author_key)
|
||||
if len(author) > 0:
|
||||
if author and len(author) > 0:
|
||||
self.parse_author(author)
|
||||
|
||||
# ProPresenter 4
|
||||
if(self.version >= 400 and self.version < 500):
|
||||
self.copyright = root.get('CCLICopyrightInfo')
|
||||
self.ccli_number = root.get('CCLILicenseNumber')
|
||||
count = 0
|
||||
for slide in root.slides.RVDisplaySlide:
|
||||
count += 1
|
||||
@ -70,5 +88,46 @@ class ProPresenterImport(SongImport):
|
||||
rtf = base64.standard_b64decode(RTFData)
|
||||
words, encoding = strip_rtf(rtf.decode())
|
||||
self.add_verse(words, "v%d" % count)
|
||||
|
||||
# ProPresenter 5
|
||||
elif(self.version >= 500 and self.version < 600):
|
||||
self.copyright = root.get('CCLICopyrightInfo')
|
||||
self.ccli_number = root.get('CCLILicenseNumber')
|
||||
count = 0
|
||||
for group in root.groups.RVSlideGrouping:
|
||||
for slide in group.slides.RVDisplaySlide:
|
||||
count += 1
|
||||
if not hasattr(slide.displayElements, 'RVTextElement'):
|
||||
log.debug('No text found, may be an image slide')
|
||||
continue
|
||||
RTFData = slide.displayElements.RVTextElement.get('RTFData')
|
||||
rtf = base64.standard_b64decode(RTFData)
|
||||
words, encoding = strip_rtf(rtf.decode())
|
||||
self.add_verse(words, "v%d" % count)
|
||||
|
||||
# ProPresenter 6
|
||||
elif(self.version >= 600 and self.version < 700):
|
||||
self.copyright = root.get('CCLICopyrightYear')
|
||||
self.ccli_number = root.get('CCLISongNumber')
|
||||
count = 0
|
||||
for group in root.array.RVSlideGrouping:
|
||||
for slide in group.array.RVDisplaySlide:
|
||||
count += 1
|
||||
for item in slide.array:
|
||||
if not (item.get('rvXMLIvarName') == "displayElements"):
|
||||
continue
|
||||
if not hasattr(item, 'RVTextElement'):
|
||||
log.debug('No text found, may be an image slide')
|
||||
continue
|
||||
for contents in item.RVTextElement.NSString:
|
||||
b64Data = contents.text
|
||||
data = base64.standard_b64decode(b64Data)
|
||||
words = None
|
||||
if(contents.get('rvXMLIvarName') == "RTFData"):
|
||||
words, encoding = strip_rtf(data.decode())
|
||||
break
|
||||
if words:
|
||||
self.add_verse(words, "v%d" % count)
|
||||
|
||||
if not self.finish():
|
||||
self.log_error(self.import_source)
|
||||
|
@ -371,7 +371,7 @@ class SongImport(QtCore.QObject):
|
||||
song_book = self.manager.get_object_filtered(Book, Book.name == self.song_book_name)
|
||||
if song_book is None:
|
||||
song_book = Book.populate(name=self.song_book_name, publisher=self.song_book_pub)
|
||||
song.book = song_book
|
||||
song.add_songbook_entry(song_book, song.song_number)
|
||||
for topic_text in self.topics:
|
||||
if not topic_text:
|
||||
continue
|
||||
|
@ -26,18 +26,19 @@ import os
|
||||
import shutil
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from sqlalchemy.sql import or_
|
||||
from sqlalchemy.sql import and_, or_
|
||||
|
||||
from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
|
||||
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \
|
||||
check_item_selected, create_separated_list
|
||||
from openlp.core.lib.ui import create_widget_action
|
||||
from openlp.core.common.languagemanager import get_natural_key
|
||||
from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
|
||||
from openlp.plugins.songs.forms.songimportform import SongImportForm
|
||||
from openlp.plugins.songs.forms.songexportform import SongExportForm
|
||||
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
|
||||
from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry
|
||||
from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry, Topic
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML
|
||||
|
||||
@ -52,8 +53,11 @@ class SongSearch(object):
|
||||
Titles = 2
|
||||
Lyrics = 3
|
||||
Authors = 4
|
||||
Books = 5
|
||||
Themes = 6
|
||||
Topics = 5
|
||||
Books = 6
|
||||
Themes = 7
|
||||
Copyright = 8
|
||||
CCLInumber = 9
|
||||
|
||||
|
||||
class SongMediaItem(MediaManagerItem):
|
||||
@ -151,9 +155,17 @@ class SongMediaItem(MediaManagerItem):
|
||||
translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
|
||||
(SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
|
||||
translate('SongsPlugin.MediaItem', 'Search Authors...')),
|
||||
(SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics,
|
||||
translate('SongsPlugin.MediaItem', 'Search Topics...')),
|
||||
(SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
|
||||
translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
|
||||
(SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes)
|
||||
(SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes),
|
||||
(SongSearch.Copyright, ':/songs/song_search_copy.png',
|
||||
translate('SongsPlugin.MediaItem', 'Copyright'),
|
||||
translate('SongsPlugin.MediaItem', 'Search Copyright...')),
|
||||
(SongSearch.CCLInumber, ':/songs/song_search_ccli.png',
|
||||
translate('SongsPlugin.MediaItem', 'CCLI number'),
|
||||
translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
|
||||
])
|
||||
self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
|
||||
self.config_update()
|
||||
@ -184,14 +196,39 @@ class SongMediaItem(MediaManagerItem):
|
||||
search_results = self.plugin.manager.get_all_objects(
|
||||
Author, Author.display_name.like(search_string), Author.display_name.asc())
|
||||
self.display_results_author(search_results)
|
||||
elif search_type == SongSearch.Topics:
|
||||
log.debug('Topics Search')
|
||||
search_string = '%' + search_keywords + '%'
|
||||
search_results = self.plugin.manager.get_all_objects(
|
||||
Topic, Topic.name.like(search_string), Topic.name.asc())
|
||||
self.display_results_topic(search_results)
|
||||
elif search_type == SongSearch.Books:
|
||||
log.debug('Songbook Search')
|
||||
self.display_results_book(search_keywords)
|
||||
search_keywords = search_keywords.rpartition(' ')
|
||||
search_book = search_keywords[0] + '%'
|
||||
search_entry = search_keywords[2] + '%'
|
||||
search_results = (self.plugin.manager.session.query(SongBookEntry)
|
||||
.join(Book)
|
||||
.filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all())
|
||||
self.display_results_book(search_results)
|
||||
elif search_type == SongSearch.Themes:
|
||||
log.debug('Theme Search')
|
||||
search_string = '%' + search_keywords + '%'
|
||||
search_results = self.plugin.manager.get_all_objects(Song, Song.theme_name.like(search_string))
|
||||
search_results = self.plugin.manager.get_all_objects(
|
||||
Song, Song.theme_name.like(search_string), Song.theme_name.asc())
|
||||
self.display_results_themes(search_results)
|
||||
elif search_type == SongSearch.Copyright:
|
||||
log.debug('Copyright Search')
|
||||
search_string = '%' + search_keywords + '%'
|
||||
search_results = self.plugin.manager.get_all_objects(
|
||||
Song, and_(Song.copyright.like(search_string), Song.copyright != ''))
|
||||
self.display_results_song(search_results)
|
||||
elif search_type == SongSearch.CCLInumber:
|
||||
log.debug('CCLI number Search')
|
||||
search_string = '%' + search_keywords + '%'
|
||||
search_results = self.plugin.manager.get_all_objects(
|
||||
Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''))
|
||||
self.display_results_cclinumber(search_results)
|
||||
self.check_search_result()
|
||||
|
||||
def search_entire(self, search_keywords):
|
||||
@ -215,6 +252,12 @@ class SongMediaItem(MediaManagerItem):
|
||||
log.debug('on_song_list_load - finished')
|
||||
|
||||
def display_results_song(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list
|
||||
|
||||
:param search_results: A list of db Song objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results Song')
|
||||
self.save_auto_select_id()
|
||||
self.list_view.clear()
|
||||
@ -234,10 +277,18 @@ class SongMediaItem(MediaManagerItem):
|
||||
self.auto_select_id = -1
|
||||
|
||||
def display_results_author(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list, grouped by author
|
||||
|
||||
:param search_results: A list of db Author objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results Author')
|
||||
self.list_view.clear()
|
||||
search_results = sorted(search_results, key=lambda author: get_natural_key(author.display_name))
|
||||
for author in search_results:
|
||||
for song in author.songs:
|
||||
songs = sorted(author.songs, key=lambda song: song.sort_key)
|
||||
for song in songs:
|
||||
# Do not display temporary songs
|
||||
if song.temporary:
|
||||
continue
|
||||
@ -246,30 +297,86 @@ class SongMediaItem(MediaManagerItem):
|
||||
song_name.setData(QtCore.Qt.UserRole, song.id)
|
||||
self.list_view.addItem(song_name)
|
||||
|
||||
def display_results_book(self, search_keywords):
|
||||
def display_results_book(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list, grouped by book and entry
|
||||
|
||||
:param search_results: A list of db SongBookEntry objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results Book')
|
||||
self.list_view.clear()
|
||||
|
||||
search_keywords = search_keywords.rpartition(' ')
|
||||
search_book = search_keywords[0]
|
||||
search_entry = re.sub(r'[^0-9]', '', search_keywords[2])
|
||||
|
||||
songbook_entries = (self.plugin.manager.session.query(SongBookEntry)
|
||||
.join(Book)
|
||||
.order_by(Book.name)
|
||||
.order_by(SongBookEntry.entry))
|
||||
for songbook_entry in songbook_entries:
|
||||
search_results = sorted(search_results, key=lambda songbook_entry:
|
||||
(get_natural_key(songbook_entry.songbook.name), get_natural_key(songbook_entry.entry)))
|
||||
for songbook_entry in search_results:
|
||||
if songbook_entry.song.temporary:
|
||||
continue
|
||||
if search_book.lower() not in songbook_entry.songbook.name.lower():
|
||||
continue
|
||||
if search_entry not in songbook_entry.entry:
|
||||
continue
|
||||
song_detail = '%s #%s: %s' % (songbook_entry.songbook.name, songbook_entry.entry, songbook_entry.song.title)
|
||||
song_name = QtWidgets.QListWidgetItem(song_detail)
|
||||
song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id)
|
||||
self.list_view.addItem(song_name)
|
||||
|
||||
def display_results_topic(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list, grouped by topic
|
||||
|
||||
:param search_results: A list of db Topic objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results Topic')
|
||||
self.list_view.clear()
|
||||
search_results = sorted(search_results, key=lambda topic: get_natural_key(topic.name))
|
||||
for topic in search_results:
|
||||
songs = sorted(topic.songs, key=lambda song: song.sort_key)
|
||||
for song in songs:
|
||||
# Do not display temporary songs
|
||||
if song.temporary:
|
||||
continue
|
||||
song_detail = '%s (%s)' % (topic.name, song.title)
|
||||
song_name = QtWidgets.QListWidgetItem(song_detail)
|
||||
song_name.setData(QtCore.Qt.UserRole, song.id)
|
||||
self.list_view.addItem(song_name)
|
||||
|
||||
def display_results_themes(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list, sorted by theme
|
||||
|
||||
:param search_results: A list of db Song objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results Themes')
|
||||
self.list_view.clear()
|
||||
search_results = sorted(search_results, key=lambda song: (get_natural_key(song.theme_name),
|
||||
song.sort_key))
|
||||
for song in search_results:
|
||||
# Do not display temporary songs
|
||||
if song.temporary:
|
||||
continue
|
||||
song_detail = '%s (%s)' % (song.theme_name, song.title)
|
||||
song_name = QtWidgets.QListWidgetItem(song_detail)
|
||||
song_name.setData(QtCore.Qt.UserRole, song.id)
|
||||
self.list_view.addItem(song_name)
|
||||
|
||||
def display_results_cclinumber(self, search_results):
|
||||
"""
|
||||
Display the song search results in the media manager list, sorted by CCLI number
|
||||
|
||||
:param search_results: A list of db Song objects
|
||||
:return: None
|
||||
"""
|
||||
log.debug('display results CCLI number')
|
||||
self.list_view.clear()
|
||||
songs = sorted(search_results, key=lambda song: (get_natural_key(song.ccli_number),
|
||||
song.sort_key))
|
||||
for song in songs:
|
||||
# Do not display temporary songs
|
||||
if song.temporary:
|
||||
continue
|
||||
song_detail = '%s (%s)' % (song.ccli_number, song.title)
|
||||
song_name = QtWidgets.QListWidgetItem(song_detail)
|
||||
song_name.setData(QtCore.Qt.UserRole, song.id)
|
||||
self.list_view.addItem(song_name)
|
||||
|
||||
def on_clear_text_button_click(self):
|
||||
"""
|
||||
Clear the search text.
|
||||
|
@ -28,8 +28,7 @@ import os
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from openlp.core.common import RegistryProperties, check_directory_exists, translate
|
||||
from openlp.core.utils import clean_filename
|
||||
from openlp.core.common import RegistryProperties, check_directory_exists, translate, clean_filename
|
||||
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -62,10 +62,10 @@ import re
|
||||
from lxml import etree, objectify
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.common.versionchecker import get_application_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
|
||||
from openlp.core.utils import get_application_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -28,8 +28,8 @@ import logging
|
||||
from sqlalchemy import Table, Column, ForeignKey, types
|
||||
from sqlalchemy.sql.expression import func, false, null, text
|
||||
|
||||
from openlp.core.common.db import drop_columns
|
||||
from openlp.core.lib.db import get_upgrade_op
|
||||
from openlp.core.utils.db import drop_columns
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__version__ = 5
|
||||
|
@ -26,27 +26,26 @@ for the Songs plugin.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from tempfile import gettempdir
|
||||
import sqlite3
|
||||
from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import UiStrings, Registry, translate
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
||||
from openlp.plugins.songs.forms.songselectform import SongSelectForm
|
||||
from openlp.plugins.songs.lib import clean_song, upgrade
|
||||
from openlp.plugins.songs.lib.db import init_schema, Song
|
||||
from openlp.plugins.songs.lib.mediaitem import SongSearch
|
||||
from openlp.plugins.songs.lib.importer import SongFormat
|
||||
from openlp.plugins.songs.lib.importers.openlp import OpenLPSongImport
|
||||
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||
from openlp.plugins.songs.lib.mediaitem import SongSearch
|
||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__default_settings__ = {
|
||||
'songs/db type': 'sqlite',
|
||||
|
@ -26,10 +26,10 @@ from datetime import datetime
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, Settings, translate
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.utils.actions import ActionList
|
||||
from openlp.plugins.songusage.forms import SongUsageDetailForm, SongUsageDeleteForm
|
||||
from openlp.plugins.songusage.lib import upgrade
|
||||
from openlp.plugins.songusage.lib.db import init_schema, SongUsageItem
|
||||
|
@ -3,8 +3,11 @@
|
||||
<file>song_search_stop.png</file>
|
||||
<file>song_search_all.png</file>
|
||||
<file>song_search_author.png</file>
|
||||
<file>song_search_ccli.png</file>
|
||||
<file>song_search_copy.png</file>
|
||||
<file>song_search_lyrics.png</file>
|
||||
<file>song_search_title.png</file>
|
||||
<file>song_search_topic.png</file>
|
||||
<file>topic_edit.png</file>
|
||||
<file>author_add.png</file>
|
||||
<file>author_delete.png</file>
|
||||
|
BIN
resources/images/song_search_ccli.png
Normal file
BIN
resources/images/song_search_ccli.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 403 B |
BIN
resources/images/song_search_copy.png
Normal file
BIN
resources/images/song_search_copy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 498 B |
BIN
resources/images/song_search_topic.png
Normal file
BIN
resources/images/song_search_topic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 993 B |
4
setup.py
4
setup.py
@ -65,8 +65,8 @@ def natural_sort(seq):
|
||||
return temp
|
||||
|
||||
|
||||
# NOTE: The following code is a duplicate of the code in openlp/core/utils/__init__.py. Any fix applied here should also
|
||||
# be applied there.
|
||||
# NOTE: The following code is a duplicate of the code in openlp/core/common/checkversion.py.
|
||||
# Any fix applied here should also be applied there.
|
||||
ver_file = None
|
||||
try:
|
||||
# Get the revision of this tree.
|
||||
|
0
tests/functional/openlp_core/__init__.py
Normal file
0
tests/functional/openlp_core/__init__.py
Normal file
@ -37,7 +37,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = []
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
@ -54,7 +54,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
@ -71,7 +71,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['--portable']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
@ -88,7 +88,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug', '-d']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertTrue(args.dev_version, 'The dev_version flag should be True')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
@ -105,7 +105,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['dummy_temp']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
@ -122,7 +122,7 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug', 'dummy_temp']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
args = parse_options(None)
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
@ -130,15 +130,3 @@ class TestInitFunctions(TestMixin, TestCase):
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
|
||||
|
||||
def parse_options_two_files_test(self):
|
||||
"""
|
||||
Test the parse options process works with a file
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['dummy_temp', 'dummy_temp2']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertEquals(args, None, 'The args should be None')
|
||||
|
@ -20,15 +20,14 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.utils.actions package.
|
||||
Package to test the openlp.core.common.actions package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.utils import ActionList
|
||||
from openlp.core.utils.actions import CategoryActionList
|
||||
from openlp.core.common.actions import CategoryActionList, ActionList
|
||||
from tests.functional import MagicMock
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
@ -171,7 +171,7 @@ class TestAppLocation(TestCase):
|
||||
"""
|
||||
Test the _get_frozen_path() function when the application is not frozen (compiled by PyInstaller)
|
||||
"""
|
||||
with patch('openlp.core.utils.sys') as mocked_sys:
|
||||
with patch('openlp.core.common.sys') as mocked_sys:
|
||||
# GIVEN: The sys module "without" a "frozen" attribute
|
||||
mocked_sys.frozen = None
|
||||
|
||||
|
@ -20,19 +20,19 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.utils.db package.
|
||||
Package to test the openlp.core.common.db package.
|
||||
"""
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
import gc
|
||||
import os
|
||||
import shutil
|
||||
import sqlalchemy
|
||||
import time
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.utils.db import drop_column, drop_columns
|
||||
import sqlalchemy
|
||||
|
||||
from openlp.core.common.db import drop_column, drop_columns
|
||||
from openlp.core.lib.db import init_db, get_upgrade_op
|
||||
|
||||
from tests.utils.constants import TEST_RESOURCES_PATH
|
||||
|
||||
|
342
tests/functional/openlp_core_common/test_init.py
Normal file
342
tests/functional/openlp_core_common/test_init.py
Normal file
@ -0,0 +1,342 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.common import add_actions, get_uno_instance, get_uno_command, delete_file, get_filesystem_encoding, \
|
||||
split_filename, clean_filename
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestInit(TestCase, TestMixin):
|
||||
"""
|
||||
A test suite to test out various methods around the common __init__ class.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an instance and a few example actions.
|
||||
"""
|
||||
self.build_settings()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up
|
||||
"""
|
||||
self.destroy_settings()
|
||||
|
||||
def add_actions_empty_list_test(self):
|
||||
"""
|
||||
Test that no actions are added when the list is empty
|
||||
"""
|
||||
# GIVEN: a mocked action list, and an empty list
|
||||
mocked_target = MagicMock()
|
||||
empty_list = []
|
||||
|
||||
# WHEN: The empty list is added to the mocked target
|
||||
add_actions(mocked_target, empty_list)
|
||||
|
||||
# THEN: The add method on the mocked target is never called
|
||||
self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
|
||||
self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')
|
||||
|
||||
def add_actions_none_action_test(self):
|
||||
"""
|
||||
Test that a separator is added when a None action is in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with None in it
|
||||
mocked_target = MagicMock()
|
||||
separator_list = [None]
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, separator_list)
|
||||
|
||||
# THEN: The addSeparator method is called, but the addAction method is never called
|
||||
mocked_target.addSeparator.assert_called_with()
|
||||
self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')
|
||||
|
||||
def add_actions_add_action_test(self):
|
||||
"""
|
||||
Test that an action is added when a valid action is in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with an action in it
|
||||
mocked_target = MagicMock()
|
||||
action_list = ['action']
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, action_list)
|
||||
|
||||
# THEN: The addSeparator method is not called, and the addAction method is called
|
||||
self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
|
||||
mocked_target.addAction.assert_called_with('action')
|
||||
|
||||
def add_actions_action_and_none_test(self):
|
||||
"""
|
||||
Test that an action and a separator are added when a valid action and None are in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with an action and None in it
|
||||
mocked_target = MagicMock()
|
||||
action_list = ['action', None]
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, action_list)
|
||||
|
||||
# THEN: The addSeparator method is called, and the addAction method is called
|
||||
mocked_target.addSeparator.assert_called_with()
|
||||
mocked_target.addAction.assert_called_with('action')
|
||||
|
||||
def get_uno_instance_pipe_test(self):
|
||||
"""
|
||||
Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI
|
||||
"""
|
||||
# GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe"
|
||||
mock_resolver = MagicMock()
|
||||
|
||||
# WHEN: get_uno_instance() is called
|
||||
get_uno_instance(mock_resolver)
|
||||
|
||||
# THEN: the resolve method is called with the correct argument
|
||||
mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
|
||||
|
||||
def get_uno_instance_socket_test(self):
|
||||
"""
|
||||
Test that when the UNO connection type is other than "pipe" the resolver is given the "socket" URI
|
||||
"""
|
||||
# GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "socket"
|
||||
mock_resolver = MagicMock()
|
||||
|
||||
# WHEN: get_uno_instance() is called
|
||||
get_uno_instance(mock_resolver, 'socket')
|
||||
|
||||
# THEN: the resolve method is called with the correct argument
|
||||
mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
|
||||
|
||||
def get_uno_command_libreoffice_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function uses the libreoffice command when available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns a path when called with 'libreoffice'
|
||||
with patch('openlp.core.common.which',
|
||||
**{'side_effect': lambda command: {'libreoffice': '/usr/bin/libreoffice'}[command]}):
|
||||
# WHEN: Calling get_uno_command
|
||||
result = get_uno_command()
|
||||
|
||||
# THEN: The command 'libreoffice' should be called with the appropriate parameters
|
||||
self.assertEquals(result,
|
||||
'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=pipe,name=openlp_pipe;urp;"')
|
||||
|
||||
def get_uno_command_only_soffice_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function uses the soffice command when the libreoffice command is not available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns None when called with 'libreoffice' and a path when called with
|
||||
# 'soffice'
|
||||
with patch('openlp.core.common.which',
|
||||
**{'side_effect': lambda command: {'libreoffice': None, 'soffice': '/usr/bin/soffice'}[
|
||||
command]}):
|
||||
# WHEN: Calling get_uno_command
|
||||
result = get_uno_command()
|
||||
|
||||
# THEN: The command 'soffice' should be called with the appropriate parameters
|
||||
self.assertEquals(result, 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=pipe,name=openlp_pipe;urp;"')
|
||||
|
||||
def get_uno_command_when_no_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function raises an FileNotFoundError when neither the libreoffice or soffice
|
||||
commands are available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns None
|
||||
with patch('openlp.core.common.which', **{'return_value': None}):
|
||||
# WHEN: Calling get_uno_command
|
||||
|
||||
# THEN: a FileNotFoundError exception should be raised
|
||||
self.assertRaises(FileNotFoundError, get_uno_command)
|
||||
|
||||
def get_uno_command_connection_type_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function when the connection type is anything other than pipe.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns 'libreoffice'
|
||||
with patch('openlp.core.common.which', **{'return_value': 'libreoffice'}):
|
||||
# WHEN: Calling get_uno_command with a connection type other than pipe
|
||||
result = get_uno_command('socket')
|
||||
|
||||
# THEN: The connection parameters should be set for socket
|
||||
self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=socket,host=localhost,port=2002;urp;"')
|
||||
|
||||
def get_filesystem_encoding_sys_function_not_called_test(self):
|
||||
"""
|
||||
Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function
|
||||
"""
|
||||
# GIVEN: sys.getfilesystemencoding returns "cp1252"
|
||||
with patch('openlp.core.common.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
|
||||
patch('openlp.core.common.sys.getdefaultencoding') as mocked_getdefaultencoding:
|
||||
mocked_getfilesystemencoding.return_value = 'cp1252'
|
||||
|
||||
# WHEN: get_filesystem_encoding() is called
|
||||
result = get_filesystem_encoding()
|
||||
|
||||
# THEN: getdefaultencoding should have been called
|
||||
mocked_getfilesystemencoding.assert_called_with()
|
||||
self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called')
|
||||
self.assertEqual('cp1252', result, 'The result should be "cp1252"')
|
||||
|
||||
def get_filesystem_encoding_sys_function_is_called_test(self):
|
||||
"""
|
||||
Test the get_filesystem_encoding() function calls the sys.getdefaultencoding() function
|
||||
"""
|
||||
# GIVEN: sys.getfilesystemencoding returns None and sys.getdefaultencoding returns "utf-8"
|
||||
with patch('openlp.core.common.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
|
||||
patch('openlp.core.common.sys.getdefaultencoding') as mocked_getdefaultencoding:
|
||||
mocked_getfilesystemencoding.return_value = None
|
||||
mocked_getdefaultencoding.return_value = 'utf-8'
|
||||
|
||||
# WHEN: get_filesystem_encoding() is called
|
||||
result = get_filesystem_encoding()
|
||||
|
||||
# THEN: getdefaultencoding should have been called
|
||||
mocked_getfilesystemencoding.assert_called_with()
|
||||
mocked_getdefaultencoding.assert_called_with()
|
||||
self.assertEqual('utf-8', result, 'The result should be "utf-8"')
|
||||
|
||||
def split_filename_with_file_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a file
|
||||
"""
|
||||
# GIVEN: A path to a file.
|
||||
if os.name == 'nt':
|
||||
file_path = 'C:\\home\\user\\myfile.txt'
|
||||
wanted_result = ('C:\\home\\user', 'myfile.txt')
|
||||
else:
|
||||
file_path = '/home/user/myfile.txt'
|
||||
wanted_result = ('/home/user', 'myfile.txt')
|
||||
with patch('openlp.core.common.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = True
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned')
|
||||
|
||||
def split_filename_with_dir_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a directory
|
||||
"""
|
||||
# GIVEN: A path to a dir.
|
||||
if os.name == 'nt':
|
||||
file_path = 'C:\\home\\user\\mydir'
|
||||
wanted_result = ('C:\\home\\user\\mydir', '')
|
||||
else:
|
||||
file_path = '/home/user/mydir'
|
||||
wanted_result = ('/home/user/mydir', '')
|
||||
with patch('openlp.core.common.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = False
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
self.assertEqual(wanted_result, result,
|
||||
'A two-entry tuple with the directory and file name (empty) should have been returned.')
|
||||
|
||||
def clean_filename_test(self):
|
||||
"""
|
||||
Test the clean_filename() function
|
||||
"""
|
||||
# GIVEN: A invalid file name and the valid file name.
|
||||
invalid_name = 'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py'
|
||||
wanted_name = 'A_file_with_invalid_characters______________________.py'
|
||||
|
||||
# WHEN: Clean the name.
|
||||
result = clean_filename(invalid_name)
|
||||
|
||||
# THEN: The file name should be cleaned.
|
||||
self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.')
|
||||
|
||||
def delete_file_no_path_test(self):
|
||||
"""
|
||||
Test the delete_file function when called with out a valid path
|
||||
"""
|
||||
# GIVEN: A blank path
|
||||
# WEHN: Calling delete_file
|
||||
result = delete_file('')
|
||||
|
||||
# THEN: delete_file should return False
|
||||
self.assertFalse(result, "delete_file should return False when called with ''")
|
||||
|
||||
def delete_file_path_success_test(self):
|
||||
"""
|
||||
Test the delete_file function when it successfully deletes a file
|
||||
"""
|
||||
# GIVEN: A mocked os which returns True when os.path.exists is called
|
||||
with patch('openlp.core.common.os', **{'path.exists.return_value': False}):
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should return True
|
||||
self.assertTrue(result, 'delete_file should return True when it successfully deletes a file')
|
||||
|
||||
def delete_file_path_no_file_exists_test(self):
|
||||
"""
|
||||
Test the delete_file function when the file to remove does not exist
|
||||
"""
|
||||
# GIVEN: A mocked os which returns False when os.path.exists is called
|
||||
with patch('openlp.core.common.os', **{'path.exists.return_value': False}):
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should return True
|
||||
self.assertTrue(result, 'delete_file should return True when the file doesnt exist')
|
||||
|
||||
def delete_file_path_exception_test(self):
|
||||
"""
|
||||
Test the delete_file function when os.remove raises an exception
|
||||
"""
|
||||
# GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is
|
||||
# called.
|
||||
with patch('openlp.core.common.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \
|
||||
patch('openlp.core.common.log') as mocked_log:
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should log and exception and return False
|
||||
self.assertEqual(mocked_log.exception.call_count, 1)
|
||||
self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError')
|
66
tests/functional/openlp_core_common/test_languagemanager.py
Normal file
66
tests/functional/openlp_core_common/test_languagemanager.py
Normal file
@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import patch
|
||||
from openlp.core.common.languagemanager import get_locale_key, get_natural_key
|
||||
|
||||
|
||||
class TestLanguageManager(TestCase):
|
||||
"""
|
||||
A test suite to test out various methods around the common __init__ class.
|
||||
"""
|
||||
|
||||
def get_locale_key_test(self):
|
||||
"""
|
||||
Test the get_locale_key(string) function
|
||||
"""
|
||||
with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is German
|
||||
# 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
|
||||
mocked_get_language.return_value = 'de'
|
||||
unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung']
|
||||
|
||||
# WHEN: We sort the list and use get_locale_key() to generate the sorting keys
|
||||
sorted_list = sorted(unsorted_list, key=get_locale_key)
|
||||
|
||||
# THEN: We get a properly sorted list
|
||||
self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list,
|
||||
'Strings should be sorted properly')
|
||||
|
||||
def get_natural_key_test(self):
|
||||
"""
|
||||
Test the get_natural_key(string) function
|
||||
"""
|
||||
with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is English (a language, which sorts digits before letters)
|
||||
mocked_get_language.return_value = 'en'
|
||||
unsorted_list = ['item 10a', 'item 3b', '1st item']
|
||||
|
||||
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
|
||||
sorted_list = sorted(unsorted_list, key=get_natural_key)
|
||||
|
||||
# THEN: We get a properly sorted list
|
||||
self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally')
|
63
tests/functional/openlp_core_common/test_versionchecker.py
Normal file
63
tests/functional/openlp_core_common/test_versionchecker.py
Normal file
@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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 openlp.core.common.settings import Settings
|
||||
from openlp.core.common.versionchecker import VersionThread
|
||||
from tests.functional import MagicMock, patch
|
||||
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 version_thread_triggered_test(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')
|
@ -26,6 +26,7 @@ Package to test the openlp.core.lib.projector.pjlink1 package.
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
||||
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING
|
||||
|
||||
from tests.functional import patch
|
||||
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE
|
||||
@ -60,3 +61,49 @@ class TestPJLink(TestCase):
|
||||
"Connection request should have been called with TEST_SALT"))
|
||||
self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN,
|
||||
"Connection request should have been called with TEST_PIN"))
|
||||
|
||||
def non_standard_class_reply_test(self):
|
||||
"""
|
||||
bugfix 1550891 - CLSS request returns non-standard 'Class N' reply
|
||||
"""
|
||||
# GIVEN: Test object
|
||||
pjlink = pjlink_test
|
||||
|
||||
# WHEN: Process non-standard reply
|
||||
pjlink.process_clss('Class 1')
|
||||
|
||||
# THEN: Projector class should be set with proper value
|
||||
self.assertEquals(pjlink.pjlink_class, '1',
|
||||
'Non-standard class reply should have set proper class')
|
||||
|
||||
@patch.object(pjlink_test, 'change_status')
|
||||
def status_change_test(self, mock_change_status):
|
||||
"""
|
||||
Test process_command call with ERR2 (Parameter) status
|
||||
"""
|
||||
# GIVEN: Test object
|
||||
pjlink = pjlink_test
|
||||
|
||||
# WHEN: process_command is called with "ERR2" status from projector
|
||||
pjlink.process_command('POWR', 'ERR2')
|
||||
|
||||
# THEN: change_status should have called change_status with E_UNDEFINED
|
||||
# as first parameter
|
||||
mock_change_status.called_with(E_PARAMETER,
|
||||
'change_status should have been called with "{}"'.format(
|
||||
ERROR_STRING[E_PARAMETER]))
|
||||
|
||||
@patch.object(pjlink_test, 'process_inpt')
|
||||
def projector_return_ok_test(self, mock_process_inpt):
|
||||
"""
|
||||
Test projector calls process_inpt command when process_command is called with INPT option
|
||||
"""
|
||||
# GIVEN: Test object
|
||||
pjlink = pjlink_test
|
||||
|
||||
# WHEN: process_command is called with INST command and 31 input:
|
||||
pjlink.process_command('INPT', '31')
|
||||
|
||||
# THEN: process_inpt method should have been called with 31
|
||||
mock_process_inpt.called_with('31',
|
||||
"process_inpt should have been called with 31")
|
||||
|
229
tests/functional/openlp_core_lib/test_webpagereader.py
Normal file
229
tests/functional/openlp_core_lib/test_webpagereader.py
Normal file
@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.lib.webpagereader import _get_user_agent, get_web_page
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestUtils(TestCase):
|
||||
"""
|
||||
A test suite to test out various methods around the AppLocation class.
|
||||
"""
|
||||
def get_user_agent_linux_test(self):
|
||||
"""
|
||||
Test that getting a user agent on Linux returns a user agent suitable for Linux
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'linux2'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
result = 'Linux' in user_agent or 'CrOS' in user_agent
|
||||
self.assertTrue(result, 'The user agent should be a valid Linux user agent')
|
||||
|
||||
def get_user_agent_windows_test(self):
|
||||
"""
|
||||
Test that getting a user agent on Windows returns a user agent suitable for Windows
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'win32'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent')
|
||||
|
||||
def get_user_agent_macos_test(self):
|
||||
"""
|
||||
Test that getting a user agent on OS X returns a user agent suitable for OS X
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'darwin'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent')
|
||||
|
||||
def get_user_agent_default_test(self):
|
||||
"""
|
||||
Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'freebsd'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent')
|
||||
|
||||
def get_web_page_no_url_test(self):
|
||||
"""
|
||||
Test that sending a URL of None to the get_web_page method returns None
|
||||
"""
|
||||
# GIVEN: A None url
|
||||
test_url = None
|
||||
|
||||
# WHEN: We try to get the test URL
|
||||
result = get_web_page(test_url)
|
||||
|
||||
# THEN: None should be returned
|
||||
self.assertIsNone(result, 'The return value of get_web_page should be None')
|
||||
|
||||
def get_web_page_test(self):
|
||||
"""
|
||||
Test that the get_web_page method works correctly
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.lib.webpagereader._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'
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_with_header_test(self):
|
||||
"""
|
||||
Test that adding a header to the call to get_web_page() adds the header to the request
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.lib.webpagereader._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')
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=fake_header)
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_with_user_agent_in_headers_test(self):
|
||||
"""
|
||||
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.lib.webpagereader.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.lib.webpagereader._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')
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=user_agent_header)
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_update_openlp_test(self):
|
||||
"""
|
||||
Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events()
|
||||
"""
|
||||
with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \
|
||||
patch('openlp.core.lib.webpagereader.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'
|
||||
|
||||
# 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')
|
@ -28,10 +28,10 @@ import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.functional import patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
from openlp.core.utils import CONNECTION_TIMEOUT, CONNECTION_RETRIES, get_web_page
|
||||
from openlp.core.lib.webpagereader import CONNECTION_RETRIES, get_web_page
|
||||
|
||||
|
||||
class TestFirstTimeWizard(TestMixin, TestCase):
|
@ -23,9 +23,12 @@
|
||||
Package to test the openlp.core.ui.listpreviewwidget package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from openlp.core.ui.listpreviewwidget import ListPreviewWidget
|
||||
|
||||
from tests.functional import patch
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.ui.listpreviewwidget import ListPreviewWidget
|
||||
from openlp.core.lib import ServiceItem
|
||||
|
||||
from tests.functional import MagicMock, patch, call
|
||||
|
||||
|
||||
class TestListPreviewWidget(TestCase):
|
||||
@ -34,9 +37,27 @@ class TestListPreviewWidget(TestCase):
|
||||
"""
|
||||
Mock out stuff for all the tests
|
||||
"""
|
||||
self.setup_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget._setup')
|
||||
self.mocked_setup = self.setup_patcher.start()
|
||||
self.addCleanup(self.setup_patcher.stop)
|
||||
# Mock self.parent().width()
|
||||
self.parent_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.parent')
|
||||
self.mocked_parent = self.parent_patcher.start()
|
||||
self.mocked_parent.width.return_value = 100
|
||||
self.addCleanup(self.parent_patcher.stop)
|
||||
|
||||
# Mock Settings().value()
|
||||
self.Settings_patcher = patch('openlp.core.ui.listpreviewwidget.Settings')
|
||||
self.mocked_Settings = self.Settings_patcher.start()
|
||||
self.mocked_Settings_obj = MagicMock()
|
||||
self.mocked_Settings_obj.value.return_value = None
|
||||
self.mocked_Settings.return_value = self.mocked_Settings_obj
|
||||
self.addCleanup(self.Settings_patcher.stop)
|
||||
|
||||
# Mock self.viewport().width()
|
||||
self.viewport_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport')
|
||||
self.mocked_viewport = self.viewport_patcher.start()
|
||||
self.mocked_viewport_obj = MagicMock()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
self.mocked_viewport.return_value = self.mocked_viewport_obj
|
||||
self.addCleanup(self.viewport_patcher.stop)
|
||||
|
||||
def new_list_preview_widget_test(self):
|
||||
"""
|
||||
@ -49,4 +70,206 @@ class TestListPreviewWidget(TestCase):
|
||||
|
||||
# THEN: The object is not None, and the _setup() method was called.
|
||||
self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None')
|
||||
self.mocked_setup.assert_called_with(1)
|
||||
self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc...
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# a text ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 100
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock text service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = True
|
||||
service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None},
|
||||
{'title': None, 'text': None, 'verseTag': None}]
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
# Change viewport width before forcing a resize
|
||||
self.mocked_viewport_obj.width.return_value = 400
|
||||
|
||||
# WHEN: __recalculate_layout() is called (via resizeEvent)
|
||||
list_preview_widget.resizeEvent(None)
|
||||
|
||||
# THEN: setRowHeight() should not be called, while resizeRowsToContents() should be called twice
|
||||
# (once each in __recalculate_layout and replace_service_item)
|
||||
self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called')
|
||||
self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc...
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# an image ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 0
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock image service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = False
|
||||
service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None},
|
||||
{'title': None, 'path': None, 'image': None}]
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
# Change viewport width before forcing a resize
|
||||
self.mocked_viewport_obj.width.return_value = 400
|
||||
|
||||
# WHEN: __recalculate_layout() is called (via resizeEvent)
|
||||
list_preview_widget.resizeEvent(None)
|
||||
|
||||
# THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called
|
||||
# twice for each slide.
|
||||
self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called')
|
||||
self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide')
|
||||
calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)]
|
||||
mocked_setRowHeight.assert_has_calls(calls)
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc...
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# an image ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 100
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock image service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = False
|
||||
service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None},
|
||||
{'title': None, 'path': None, 'image': None}]
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
# Change viewport width before forcing a resize
|
||||
self.mocked_viewport_obj.width.return_value = 400
|
||||
|
||||
# WHEN: __recalculate_layout() is called (via resizeEvent)
|
||||
list_preview_widget.resizeEvent(None)
|
||||
|
||||
# THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called
|
||||
# twice for each slide.
|
||||
self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called')
|
||||
self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide')
|
||||
calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)]
|
||||
mocked_setRowHeight.assert_has_calls(calls)
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget')
|
||||
def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized.
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# a text ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 100
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock text service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = True
|
||||
service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None},
|
||||
{'title': None, 'text': None, 'verseTag': None}]
|
||||
# Mock self.cellWidget().children().setMaximumWidth()
|
||||
mocked_cellWidget_child = MagicMock()
|
||||
mocked_cellWidget_obj = MagicMock()
|
||||
mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child]
|
||||
mocked_cellWidget.return_value = mocked_cellWidget_obj
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
|
||||
# WHEN: row_resized() is called
|
||||
list_preview_widget.row_resized(0, 100, 150)
|
||||
|
||||
# THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called
|
||||
self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget')
|
||||
def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized.
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# an image ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 0
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock image service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = False
|
||||
service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None},
|
||||
{'title': None, 'path': None, 'image': None}]
|
||||
# Mock self.cellWidget().children().setMaximumWidth()
|
||||
mocked_cellWidget_child = MagicMock()
|
||||
mocked_cellWidget_obj = MagicMock()
|
||||
mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child]
|
||||
mocked_cellWidget.return_value = mocked_cellWidget_obj
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
|
||||
# WHEN: row_resized() is called
|
||||
list_preview_widget.row_resized(0, 100, 150)
|
||||
|
||||
# THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called
|
||||
self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight')
|
||||
@patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget')
|
||||
def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents):
|
||||
"""
|
||||
Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized.
|
||||
"""
|
||||
# GIVEN: A setting to adjust "Max height for non-text slides in slide controller",
|
||||
# an image ServiceItem and a ListPreviewWidget.
|
||||
|
||||
# Mock Settings().value('advanced/slide max height')
|
||||
self.mocked_Settings_obj.value.return_value = 100
|
||||
# Mock self.viewport().width()
|
||||
self.mocked_viewport_obj.width.return_value = 200
|
||||
# Mock image service item
|
||||
service_item = MagicMock()
|
||||
service_item.is_text.return_value = False
|
||||
service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None},
|
||||
{'title': None, 'path': None, 'image': None}]
|
||||
# Mock self.cellWidget().children().setMaximumWidth()
|
||||
mocked_cellWidget_child = MagicMock()
|
||||
mocked_cellWidget_obj = MagicMock()
|
||||
mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child]
|
||||
mocked_cellWidget.return_value = mocked_cellWidget_obj
|
||||
# init ListPreviewWidget and load service item
|
||||
list_preview_widget = ListPreviewWidget(None, 1)
|
||||
list_preview_widget.replace_service_item(service_item, 200, 0)
|
||||
|
||||
# WHEN: row_resized() is called
|
||||
list_preview_widget.row_resized(0, 100, 150)
|
||||
|
||||
# THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called
|
||||
mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150)
|
||||
|
@ -22,13 +22,14 @@
|
||||
"""
|
||||
Package to test the openlp.core.ui.slidecontroller package.
|
||||
"""
|
||||
import PyQt5
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.common import Registry, ThemeLevel
|
||||
from openlp.core.common import Registry, ThemeLevel, Settings
|
||||
from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities
|
||||
from openlp.core.ui import ServiceManager
|
||||
|
||||
from tests.functional import MagicMock
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestServiceManager(TestCase):
|
||||
@ -540,3 +541,80 @@ class TestServiceManager(TestCase):
|
||||
self.assertEquals(service_manager.timed_slide_interval.setChecked.call_count, 0, 'Should not be called')
|
||||
self.assertEquals(service_manager.theme_menu.menuAction().setVisible.call_count, 1,
|
||||
'Should have be called once')
|
||||
|
||||
@patch(u'openlp.core.ui.servicemanager.Settings')
|
||||
@patch(u'PyQt5.QtCore.QTimer.singleShot')
|
||||
def single_click_preview_test_true(self, mocked_singleShot, MockedSettings):
|
||||
"""
|
||||
Test that when "Preview items when clicked in Service Manager" enabled the preview timer starts
|
||||
"""
|
||||
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
|
||||
mocked_settings = MagicMock()
|
||||
mocked_settings.value.return_value = True
|
||||
MockedSettings.return_value = mocked_settings
|
||||
service_manager = ServiceManager(None)
|
||||
# WHEN: on_single_click_preview() is called
|
||||
service_manager.on_single_click_preview()
|
||||
# THEN: timer should have been started
|
||||
mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),
|
||||
service_manager.on_single_click_preview_timeout)
|
||||
|
||||
@patch(u'openlp.core.ui.servicemanager.Settings')
|
||||
@patch(u'PyQt5.QtCore.QTimer.singleShot')
|
||||
def single_click_preview_test_false(self, mocked_singleShot, MockedSettings):
|
||||
"""
|
||||
Test that when "Preview items when clicked in Service Manager" disabled the preview timer doesn't start
|
||||
"""
|
||||
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
|
||||
mocked_settings = MagicMock()
|
||||
mocked_settings.value.return_value = False
|
||||
MockedSettings.return_value = mocked_settings
|
||||
service_manager = ServiceManager(None)
|
||||
# WHEN: on_single_click_preview() is called
|
||||
service_manager.on_single_click_preview()
|
||||
# THEN: timer should not be started
|
||||
self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.servicemanager.Settings')
|
||||
@patch(u'PyQt5.QtCore.QTimer.singleShot')
|
||||
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
|
||||
def single_click_preview_test_double(self, mocked_make_live, mocked_singleShot, MockedSettings):
|
||||
"""
|
||||
Test that when a double click has registered the preview timer doesn't start
|
||||
"""
|
||||
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
|
||||
mocked_settings = MagicMock()
|
||||
mocked_settings.value.return_value = True
|
||||
MockedSettings.return_value = mocked_settings
|
||||
service_manager = ServiceManager(None)
|
||||
# WHEN: on_single_click_preview() is called following a double click
|
||||
service_manager.on_double_click_live()
|
||||
service_manager.on_single_click_preview()
|
||||
# THEN: timer should not be started
|
||||
self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called')
|
||||
|
||||
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
|
||||
def single_click_timeout_test_single(self, mocked_make_preview):
|
||||
"""
|
||||
Test that when a single click has been registered, the item is sent to preview
|
||||
"""
|
||||
# GIVEN: A service manager.
|
||||
service_manager = ServiceManager(None)
|
||||
# WHEN: on_single_click_preview() is called
|
||||
service_manager.on_single_click_preview_timeout()
|
||||
# THEN: make_preview() should have been called
|
||||
self.assertEquals(mocked_make_preview.call_count, 1, 'Should have been called once')
|
||||
|
||||
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
|
||||
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
|
||||
def single_click_timeout_test_double(self, mocked_make_live, mocked_make_preview):
|
||||
"""
|
||||
Test that when a double click has been registered, the item does not goes to preview
|
||||
"""
|
||||
# GIVEN: A service manager.
|
||||
service_manager = ServiceManager(None)
|
||||
# WHEN: on_single_click_preview() is called after a double click
|
||||
service_manager.on_double_click_live()
|
||||
service_manager.on_single_click_preview_timeout()
|
||||
# THEN: make_preview() should have been called
|
||||
self.assertEquals(mocked_make_preview.call_count, 0, 'Should not be called')
|
||||
|
@ -685,6 +685,34 @@ class TestSlideController(TestCase):
|
||||
self.assertEqual('mocked_presentation_item_stop', mocked_execute.call_args_list[1][0][0],
|
||||
'The presentation should have been stopped.')
|
||||
|
||||
def live_stolen_focus_shortcuts_test(self):
|
||||
"""
|
||||
Test that all the needed shortcuts are available in scenarios where Live has stolen focus.
|
||||
These are found under def __add_actions_to_widget(self, widget): in slidecontroller.py
|
||||
"""
|
||||
# GIVEN: A slide controller, actions needed
|
||||
slide_controller = SlideController(None)
|
||||
mocked_widget = MagicMock()
|
||||
slide_controller.previous_item = MagicMock()
|
||||
slide_controller.next_item = MagicMock()
|
||||
slide_controller.previous_service = MagicMock()
|
||||
slide_controller.next_service = MagicMock()
|
||||
slide_controller.escape_item = MagicMock()
|
||||
slide_controller.desktop_screen = MagicMock()
|
||||
slide_controller.blank_screen = MagicMock()
|
||||
slide_controller.theme_screen = MagicMock()
|
||||
|
||||
# WHEN: __add_actions_to_widget is called
|
||||
slide_controller._SlideController__add_actions_to_widget(mocked_widget)
|
||||
|
||||
# THEN: The call to addActions should be correct
|
||||
mocked_widget.addActions.assert_called_with([
|
||||
slide_controller.previous_item, slide_controller.next_item,
|
||||
slide_controller.previous_service, slide_controller.next_service,
|
||||
slide_controller.escape_item, slide_controller.desktop_screen,
|
||||
slide_controller.theme_screen, slide_controller.blank_screen
|
||||
])
|
||||
|
||||
|
||||
class TestInfoLabel(TestCase):
|
||||
|
||||
|
@ -1,129 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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.utils.actions package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.utils import VersionThread, get_uno_command
|
||||
from tests.functional import MagicMock, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestInitFunctions(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 version_thread_triggered_test(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.utils.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')
|
||||
|
||||
def get_uno_command_libreoffice_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function uses the libreoffice command when available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns a path when called with 'libreoffice'
|
||||
with patch('openlp.core.utils.which',
|
||||
**{'side_effect': lambda command: {'libreoffice': '/usr/bin/libreoffice'}[command]}):
|
||||
|
||||
# WHEN: Calling get_uno_command
|
||||
result = get_uno_command()
|
||||
|
||||
# THEN: The command 'libreoffice' should be called with the appropriate parameters
|
||||
self.assertEquals(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=pipe,name=openlp_pipe;urp;"')
|
||||
|
||||
def get_uno_command_only_soffice_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function uses the soffice command when the libreoffice command is not available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns None when called with 'libreoffice' and a path when called with
|
||||
# 'soffice'
|
||||
with patch('openlp.core.utils.which',
|
||||
**{'side_effect': lambda command: {'libreoffice': None, 'soffice': '/usr/bin/soffice'}[command]}):
|
||||
|
||||
# WHEN: Calling get_uno_command
|
||||
result = get_uno_command()
|
||||
|
||||
# THEN: The command 'soffice' should be called with the appropriate parameters
|
||||
self.assertEquals(result, 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=pipe,name=openlp_pipe;urp;"')
|
||||
|
||||
def get_uno_command_when_no_command_exists_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function raises an FileNotFoundError when neither the libreoffice or soffice
|
||||
commands are available.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns None
|
||||
with patch('openlp.core.utils.which', **{'return_value': None}):
|
||||
|
||||
# WHEN: Calling get_uno_command
|
||||
|
||||
# THEN: a FileNotFoundError exception should be raised
|
||||
self.assertRaises(FileNotFoundError, get_uno_command)
|
||||
|
||||
def get_uno_command_connection_type_test(self):
|
||||
"""
|
||||
Test the ``get_uno_command`` function when the connection type is anything other than pipe.
|
||||
:return:
|
||||
"""
|
||||
|
||||
# GIVEN: A patched 'which' method which returns 'libreoffice'
|
||||
with patch('openlp.core.utils.which', **{'return_value': 'libreoffice'}):
|
||||
|
||||
# WHEN: Calling get_uno_command with a connection type other than pipe
|
||||
result = get_uno_command('socket')
|
||||
|
||||
# THEN: The connection parameters should be set for socket
|
||||
self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard'
|
||||
' "--accept=socket,host=localhost,port=2002;urp;"')
|
@ -1,491 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2016 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 #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
import os
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, get_locale_key, \
|
||||
get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance, add_actions
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestUtils(TestCase):
|
||||
"""
|
||||
A test suite to test out various methods around the AppLocation class.
|
||||
"""
|
||||
def add_actions_empty_list_test(self):
|
||||
"""
|
||||
Test that no actions are added when the list is empty
|
||||
"""
|
||||
# GIVEN: a mocked action list, and an empty list
|
||||
mocked_target = MagicMock()
|
||||
empty_list = []
|
||||
|
||||
# WHEN: The empty list is added to the mocked target
|
||||
add_actions(mocked_target, empty_list)
|
||||
|
||||
# THEN: The add method on the mocked target is never called
|
||||
self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
|
||||
self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')
|
||||
|
||||
def add_actions_none_action_test(self):
|
||||
"""
|
||||
Test that a separator is added when a None action is in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with None in it
|
||||
mocked_target = MagicMock()
|
||||
separator_list = [None]
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, separator_list)
|
||||
|
||||
# THEN: The addSeparator method is called, but the addAction method is never called
|
||||
mocked_target.addSeparator.assert_called_with()
|
||||
self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')
|
||||
|
||||
def add_actions_add_action_test(self):
|
||||
"""
|
||||
Test that an action is added when a valid action is in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with an action in it
|
||||
mocked_target = MagicMock()
|
||||
action_list = ['action']
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, action_list)
|
||||
|
||||
# THEN: The addSeparator method is not called, and the addAction method is called
|
||||
self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
|
||||
mocked_target.addAction.assert_called_with('action')
|
||||
|
||||
def add_actions_action_and_none_test(self):
|
||||
"""
|
||||
Test that an action and a separator are added when a valid action and None are in the list
|
||||
"""
|
||||
# GIVEN: a mocked action list, and a list with an action and None in it
|
||||
mocked_target = MagicMock()
|
||||
action_list = ['action', None]
|
||||
|
||||
# WHEN: The list is added to the mocked target
|
||||
add_actions(mocked_target, action_list)
|
||||
|
||||
# THEN: The addSeparator method is called, and the addAction method is called
|
||||
mocked_target.addSeparator.assert_called_with()
|
||||
mocked_target.addAction.assert_called_with('action')
|
||||
|
||||
def get_filesystem_encoding_sys_function_not_called_test(self):
|
||||
"""
|
||||
Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function
|
||||
"""
|
||||
# GIVEN: sys.getfilesystemencoding returns "cp1252"
|
||||
with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
|
||||
patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding:
|
||||
mocked_getfilesystemencoding.return_value = 'cp1252'
|
||||
|
||||
# WHEN: get_filesystem_encoding() is called
|
||||
result = get_filesystem_encoding()
|
||||
|
||||
# THEN: getdefaultencoding should have been called
|
||||
mocked_getfilesystemencoding.assert_called_with()
|
||||
self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called')
|
||||
self.assertEqual('cp1252', result, 'The result should be "cp1252"')
|
||||
|
||||
def get_filesystem_encoding_sys_function_is_called_test(self):
|
||||
"""
|
||||
Test the get_filesystem_encoding() function calls the sys.getdefaultencoding() function
|
||||
"""
|
||||
# GIVEN: sys.getfilesystemencoding returns None and sys.getdefaultencoding returns "utf-8"
|
||||
with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
|
||||
patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding:
|
||||
mocked_getfilesystemencoding.return_value = None
|
||||
mocked_getdefaultencoding.return_value = 'utf-8'
|
||||
|
||||
# WHEN: get_filesystem_encoding() is called
|
||||
result = get_filesystem_encoding()
|
||||
|
||||
# THEN: getdefaultencoding should have been called
|
||||
mocked_getfilesystemencoding.assert_called_with()
|
||||
mocked_getdefaultencoding.assert_called_with()
|
||||
self.assertEqual('utf-8', result, 'The result should be "utf-8"')
|
||||
|
||||
def split_filename_with_file_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a file
|
||||
"""
|
||||
# GIVEN: A path to a file.
|
||||
if os.name == 'nt':
|
||||
file_path = 'C:\\home\\user\\myfile.txt'
|
||||
wanted_result = ('C:\\home\\user', 'myfile.txt')
|
||||
else:
|
||||
file_path = '/home/user/myfile.txt'
|
||||
wanted_result = ('/home/user', 'myfile.txt')
|
||||
with patch('openlp.core.utils.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = True
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned')
|
||||
|
||||
def split_filename_with_dir_path_test(self):
|
||||
"""
|
||||
Test the split_filename() function with a path to a directory
|
||||
"""
|
||||
# GIVEN: A path to a dir.
|
||||
if os.name == 'nt':
|
||||
file_path = 'C:\\home\\user\\mydir'
|
||||
wanted_result = ('C:\\home\\user\\mydir', '')
|
||||
else:
|
||||
file_path = '/home/user/mydir'
|
||||
wanted_result = ('/home/user/mydir', '')
|
||||
with patch('openlp.core.utils.os.path.isfile') as mocked_is_file:
|
||||
mocked_is_file.return_value = False
|
||||
|
||||
# WHEN: Split the file name.
|
||||
result = split_filename(file_path)
|
||||
|
||||
# THEN: A tuple should be returned.
|
||||
self.assertEqual(wanted_result, result,
|
||||
'A two-entry tuple with the directory and file name (empty) should have been returned.')
|
||||
|
||||
def clean_filename_test(self):
|
||||
"""
|
||||
Test the clean_filename() function
|
||||
"""
|
||||
# GIVEN: A invalid file name and the valid file name.
|
||||
invalid_name = 'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py'
|
||||
wanted_name = 'A_file_with_invalid_characters______________________.py'
|
||||
|
||||
# WHEN: Clean the name.
|
||||
result = clean_filename(invalid_name)
|
||||
|
||||
# THEN: The file name should be cleaned.
|
||||
self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.')
|
||||
|
||||
def delete_file_no_path_test(self):
|
||||
"""
|
||||
Test the delete_file function when called with out a valid path
|
||||
"""
|
||||
# GIVEN: A blank path
|
||||
# WEHN: Calling delete_file
|
||||
result = delete_file('')
|
||||
|
||||
# THEN: delete_file should return False
|
||||
self.assertFalse(result, "delete_file should return False when called with ''")
|
||||
|
||||
def delete_file_path_success_test(self):
|
||||
"""
|
||||
Test the delete_file function when it successfully deletes a file
|
||||
"""
|
||||
# GIVEN: A mocked os which returns True when os.path.exists is called
|
||||
with patch('openlp.core.utils.os', **{'path.exists.return_value': False}):
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should return True
|
||||
self.assertTrue(result, 'delete_file should return True when it successfully deletes a file')
|
||||
|
||||
def delete_file_path_no_file_exists_test(self):
|
||||
"""
|
||||
Test the delete_file function when the file to remove does not exist
|
||||
"""
|
||||
# GIVEN: A mocked os which returns False when os.path.exists is called
|
||||
with patch('openlp.core.utils.os', **{'path.exists.return_value': False}):
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should return True
|
||||
self.assertTrue(result, 'delete_file should return True when the file doesnt exist')
|
||||
|
||||
def delete_file_path_exception_test(self):
|
||||
"""
|
||||
Test the delete_file function when os.remove raises an exception
|
||||
"""
|
||||
# GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is
|
||||
# called.
|
||||
with patch('openlp.core.utils.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \
|
||||
patch('openlp.core.utils.log') as mocked_log:
|
||||
|
||||
# WHEN: Calling delete_file with a file path
|
||||
result = delete_file('path/file.ext')
|
||||
|
||||
# THEN: delete_file should log and exception and return False
|
||||
self.assertEqual(mocked_log.exception.call_count, 1)
|
||||
self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError')
|
||||
|
||||
def get_locale_key_test(self):
|
||||
"""
|
||||
Test the get_locale_key(string) function
|
||||
"""
|
||||
with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is German
|
||||
# 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
|
||||
mocked_get_language.return_value = 'de'
|
||||
unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung']
|
||||
|
||||
# WHEN: We sort the list and use get_locale_key() to generate the sorting keys
|
||||
sorted_list = sorted(unsorted_list, key=get_locale_key)
|
||||
|
||||
# THEN: We get a properly sorted list
|
||||
self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list,
|
||||
'Strings should be sorted properly')
|
||||
|
||||
def get_natural_key_test(self):
|
||||
"""
|
||||
Test the get_natural_key(string) function
|
||||
"""
|
||||
with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
|
||||
# GIVEN: The language is English (a language, which sorts digits before letters)
|
||||
mocked_get_language.return_value = 'en'
|
||||
unsorted_list = ['item 10a', 'item 3b', '1st item']
|
||||
|
||||
# WHEN: We sort the list and use get_natural_key() to generate the sorting keys
|
||||
sorted_list = sorted(unsorted_list, key=get_natural_key)
|
||||
|
||||
# THEN: We get a properly sorted list
|
||||
self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally')
|
||||
|
||||
def get_uno_instance_pipe_test(self):
|
||||
"""
|
||||
Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI
|
||||
"""
|
||||
# GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe"
|
||||
mock_resolver = MagicMock()
|
||||
|
||||
# WHEN: get_uno_instance() is called
|
||||
get_uno_instance(mock_resolver)
|
||||
|
||||
# THEN: the resolve method is called with the correct argument
|
||||
mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
|
||||
|
||||
def get_uno_instance_socket_test(self):
|
||||
"""
|
||||
Test that when the UNO connection type is other than "pipe" the resolver is given the "socket" URI
|
||||
"""
|
||||
# GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "socket"
|
||||
mock_resolver = MagicMock()
|
||||
|
||||
# WHEN: get_uno_instance() is called
|
||||
get_uno_instance(mock_resolver, 'socket')
|
||||
|
||||
# THEN: the resolve method is called with the correct argument
|
||||
mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
|
||||
|
||||
def get_user_agent_linux_test(self):
|
||||
"""
|
||||
Test that getting a user agent on Linux returns a user agent suitable for Linux
|
||||
"""
|
||||
with patch('openlp.core.utils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'linux2'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
result = 'Linux' in user_agent or 'CrOS' in user_agent
|
||||
self.assertTrue(result, 'The user agent should be a valid Linux user agent')
|
||||
|
||||
def get_user_agent_windows_test(self):
|
||||
"""
|
||||
Test that getting a user agent on Windows returns a user agent suitable for Windows
|
||||
"""
|
||||
with patch('openlp.core.utils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'win32'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent')
|
||||
|
||||
def get_user_agent_macos_test(self):
|
||||
"""
|
||||
Test that getting a user agent on OS X returns a user agent suitable for OS X
|
||||
"""
|
||||
with patch('openlp.core.utils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'darwin'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent')
|
||||
|
||||
def get_user_agent_default_test(self):
|
||||
"""
|
||||
Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent
|
||||
"""
|
||||
with patch('openlp.core.utils.sys') as mocked_sys:
|
||||
|
||||
# GIVEN: The system is Linux
|
||||
mocked_sys.platform = 'freebsd'
|
||||
|
||||
# WHEN: We call _get_user_agent()
|
||||
user_agent = _get_user_agent()
|
||||
|
||||
# THEN: The user agent is a Linux (or ChromeOS) user agent
|
||||
self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent')
|
||||
|
||||
def get_web_page_no_url_test(self):
|
||||
"""
|
||||
Test that sending a URL of None to the get_web_page method returns None
|
||||
"""
|
||||
# GIVEN: A None url
|
||||
test_url = None
|
||||
|
||||
# WHEN: We try to get the test URL
|
||||
result = get_web_page(test_url)
|
||||
|
||||
# THEN: None should be returned
|
||||
self.assertIsNone(result, 'The return value of get_web_page should be None')
|
||||
|
||||
def get_web_page_test(self):
|
||||
"""
|
||||
Test that the get_web_page method works correctly
|
||||
"""
|
||||
with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \
|
||||
patch('openlp.core.utils.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'
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_with_header_test(self):
|
||||
"""
|
||||
Test that adding a header to the call to get_web_page() adds the header to the request
|
||||
"""
|
||||
with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.utils._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')
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=fake_header)
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_with_user_agent_in_headers_test(self):
|
||||
"""
|
||||
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.utils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.utils._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')
|
||||
|
||||
# WHEN: The get_web_page() method is called
|
||||
returned_page = get_web_page(fake_url, header=user_agent_header)
|
||||
|
||||
# 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')
|
||||
|
||||
def get_web_page_update_openlp_test(self):
|
||||
"""
|
||||
Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events()
|
||||
"""
|
||||
with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
|
||||
patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
|
||||
patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \
|
||||
patch('openlp.core.utils.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'
|
||||
|
||||
# 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')
|
@ -99,7 +99,7 @@ class TestRemoteTab(TestCase, TestMixin):
|
||||
"""
|
||||
# GIVEN: A mocked location
|
||||
with patch('openlp.core.common.Settings') as mocked_class, \
|
||||
patch('openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch('openlp.core.common.applocation.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch('openlp.core.common.check_directory_exists') as mocked_check_directory_exists, \
|
||||
patch('openlp.core.common.applocation.os') as mocked_os:
|
||||
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
|
||||
@ -127,7 +127,7 @@ class TestRemoteTab(TestCase, TestMixin):
|
||||
"""
|
||||
# GIVEN: A mocked location
|
||||
with patch('openlp.core.common.Settings') as mocked_class, \
|
||||
patch('openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch('openlp.core.common.applocation.AppLocation.get_directory') as mocked_get_directory, \
|
||||
patch('openlp.core.common.check_directory_exists') as mocked_check_directory_exists, \
|
||||
patch('openlp.core.common.applocation.os') as mocked_os:
|
||||
# GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
|
||||
|
@ -48,6 +48,12 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \
|
||||
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
|
||||
self.media_item = SongMediaItem(None, MagicMock())
|
||||
self.media_item.save_auto_select_id = MagicMock()
|
||||
self.media_item.list_view = MagicMock()
|
||||
self.media_item.list_view.save_auto_select_id = MagicMock()
|
||||
self.media_item.list_view.clear = MagicMock()
|
||||
self.media_item.list_view.addItem = MagicMock()
|
||||
self.media_item.auto_select_id = -1
|
||||
self.media_item.display_songbook = False
|
||||
self.media_item.display_copyright_symbol = False
|
||||
self.setup_application()
|
||||
@ -60,6 +66,183 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
"""
|
||||
self.destroy_settings()
|
||||
|
||||
def display_results_song_test(self):
|
||||
"""
|
||||
Test displaying song search results with basic song
|
||||
"""
|
||||
# GIVEN: Search results, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_song = MagicMock()
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.authors = []
|
||||
mock_author = MagicMock()
|
||||
mock_author.display_name = 'My Author'
|
||||
mock_song.authors.append(mock_author)
|
||||
mock_song.temporary = False
|
||||
mock_search_results.append(mock_song)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results
|
||||
self.media_item.display_results_song(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
self.media_item.save_auto_select_id.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('My Song (My Author)')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def display_results_author_test(self):
|
||||
"""
|
||||
Test displaying song search results grouped by author with basic song
|
||||
"""
|
||||
# GIVEN: Search results grouped by author, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_author = MagicMock()
|
||||
mock_song = MagicMock()
|
||||
mock_author.display_name = 'My Author'
|
||||
mock_author.songs = []
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.temporary = False
|
||||
mock_author.songs.append(mock_song)
|
||||
mock_search_results.append(mock_author)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results grouped by author
|
||||
self.media_item.display_results_author(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('My Author (My Song)')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def display_results_book_test(self):
|
||||
"""
|
||||
Test displaying song search results grouped by book and entry with basic song
|
||||
"""
|
||||
# GIVEN: Search results grouped by book and entry, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_songbook_entry = MagicMock()
|
||||
mock_songbook = MagicMock()
|
||||
mock_song = MagicMock()
|
||||
mock_songbook_entry.entry = '1'
|
||||
mock_songbook.name = 'My Book'
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.temporary = False
|
||||
mock_songbook_entry.song = mock_song
|
||||
mock_songbook_entry.songbook = mock_songbook
|
||||
mock_search_results.append(mock_songbook_entry)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results grouped by book
|
||||
self.media_item.display_results_book(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('My Book #1: My Song')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_songbook_entry.song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def display_results_topic_test(self):
|
||||
"""
|
||||
Test displaying song search results grouped by topic with basic song
|
||||
"""
|
||||
# GIVEN: Search results grouped by topic, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_topic = MagicMock()
|
||||
mock_song = MagicMock()
|
||||
mock_topic.name = 'My Topic'
|
||||
mock_topic.songs = []
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.temporary = False
|
||||
mock_topic.songs.append(mock_song)
|
||||
mock_search_results.append(mock_topic)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results grouped by topic
|
||||
self.media_item.display_results_topic(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('My Topic (My Song)')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def display_results_themes_test(self):
|
||||
"""
|
||||
Test displaying song search results sorted by theme with basic song
|
||||
"""
|
||||
# GIVEN: Search results sorted by theme, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_song = MagicMock()
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.theme_name = 'My Theme'
|
||||
mock_song.temporary = False
|
||||
mock_search_results.append(mock_song)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results sorted by theme
|
||||
self.media_item.display_results_themes(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('My Theme (My Song)')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def display_results_cclinumber_test(self):
|
||||
"""
|
||||
Test displaying song search results sorted by CCLI number with basic song
|
||||
"""
|
||||
# GIVEN: Search results sorted by CCLI number, plus a mocked QtListWidgetItem
|
||||
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
|
||||
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
|
||||
mock_search_results = []
|
||||
mock_song = MagicMock()
|
||||
mock_song.id = 1
|
||||
mock_song.title = 'My Song'
|
||||
mock_song.sort_key = 'My Song'
|
||||
mock_song.ccli_number = '12345'
|
||||
mock_song.temporary = False
|
||||
mock_search_results.append(mock_song)
|
||||
mock_qlist_widget = MagicMock()
|
||||
MockedQListWidgetItem.return_value = mock_qlist_widget
|
||||
|
||||
# WHEN: I display song search results sorted by CCLI number
|
||||
self.media_item.display_results_cclinumber(mock_search_results)
|
||||
|
||||
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
|
||||
self.media_item.list_view.clear.assert_called_with()
|
||||
MockedQListWidgetItem.assert_called_with('12345 (My Song)')
|
||||
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
|
||||
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
|
||||
|
||||
def build_song_footer_one_author_test(self):
|
||||
"""
|
||||
Test build songs footer with basic song and one author
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user