forked from openlp/openlp
- Merged trunk on 14/8/16.
This commit is contained in:
commit
f3533885ec
@ -46,3 +46,4 @@ cover
|
||||
coverage
|
||||
tags
|
||||
output
|
||||
htmlcov
|
||||
|
17
nose2.cfg
17
nose2.cfg
@ -1,17 +1,22 @@
|
||||
[unittest]
|
||||
verbose = True
|
||||
verbose = true
|
||||
plugins = nose2.plugins.mp
|
||||
|
||||
[log-capture]
|
||||
always-on = True
|
||||
clear-handlers = True
|
||||
always-on = true
|
||||
clear-handlers = true
|
||||
filter = -nose
|
||||
log-level = ERROR
|
||||
|
||||
[test-result]
|
||||
always-on = True
|
||||
descriptions = True
|
||||
always-on = true
|
||||
descriptions = true
|
||||
|
||||
[coverage]
|
||||
always-on = False
|
||||
always-on = true
|
||||
coverage = openlp
|
||||
coverage-report = html
|
||||
|
||||
[multiprocess]
|
||||
always-on = false
|
||||
processes = 4
|
||||
|
@ -30,6 +30,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
@ -416,3 +417,24 @@ def check_binary_exists(program_path):
|
||||
runlog = ''
|
||||
log.debug('check_output returned: {text}'.format(text=runlog))
|
||||
return runlog
|
||||
|
||||
|
||||
def get_file_encoding(filename):
|
||||
"""
|
||||
Utility function to incrementally detect the file encoding.
|
||||
|
||||
:param filename: Filename for the file to determine the encoding for. Str
|
||||
:return: A dict with the keys 'encoding' and 'confidence'
|
||||
"""
|
||||
detector = UniversalDetector()
|
||||
try:
|
||||
with open(filename, 'rb') as detect_file:
|
||||
while not detector.done:
|
||||
chunk = detect_file.read(1024)
|
||||
if not chunk:
|
||||
break
|
||||
detector.feed(chunk)
|
||||
detector.close()
|
||||
return detector.result
|
||||
except OSError:
|
||||
log.exception('Error detecting file encoding')
|
||||
|
@ -24,9 +24,15 @@ The :mod:`~openlp.core.lib.exceptions` module contains custom exceptions
|
||||
"""
|
||||
|
||||
|
||||
# TODO: Test __init__ & __str__
|
||||
class ValidationError(Exception):
|
||||
"""
|
||||
The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating
|
||||
import files.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __init__(self, msg="Validation Error"):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return '{error_message}'.format(error_message=self.msg)
|
||||
|
@ -131,227 +131,243 @@ S_INFO = 310
|
||||
S_NETWORK_SENDING = 400
|
||||
S_NETWORK_RECEIVED = 401
|
||||
|
||||
CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
|
||||
E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
|
||||
E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
|
||||
E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
|
||||
E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
|
||||
E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
|
||||
E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
|
||||
E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
|
||||
}
|
||||
CONNECTION_ERRORS = {
|
||||
E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
|
||||
E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
|
||||
E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
|
||||
E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
|
||||
E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
|
||||
E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
|
||||
E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
|
||||
E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
|
||||
}
|
||||
|
||||
PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION, # Authentication error
|
||||
'ERR1': E_UNDEFINED, # Undefined command error
|
||||
'ERR2': E_PARAMETER, # Invalid parameter error
|
||||
'ERR3': E_UNAVAILABLE, # Projector busy
|
||||
'ERR4': E_PROJECTOR, # Projector or display failure
|
||||
E_AUTHENTICATION: 'ERRA',
|
||||
E_UNDEFINED: 'ERR1',
|
||||
E_PARAMETER: 'ERR2',
|
||||
E_UNAVAILABLE: 'ERR3',
|
||||
E_PROJECTOR: 'ERR4'}
|
||||
PJLINK_ERRORS = {
|
||||
'ERRA': E_AUTHENTICATION, # Authentication error
|
||||
'ERR1': E_UNDEFINED, # Undefined command error
|
||||
'ERR2': E_PARAMETER, # Invalid parameter error
|
||||
'ERR3': E_UNAVAILABLE, # Projector busy
|
||||
'ERR4': E_PROJECTOR, # Projector or display failure
|
||||
E_AUTHENTICATION: 'ERRA',
|
||||
E_UNDEFINED: 'ERR1',
|
||||
E_PARAMETER: 'ERR2',
|
||||
E_UNAVAILABLE: 'ERR3',
|
||||
E_PROJECTOR: 'ERR4'
|
||||
}
|
||||
|
||||
# Map error/status codes to string
|
||||
ERROR_STRING = {0: 'S_OK',
|
||||
E_GENERAL: 'E_GENERAL',
|
||||
E_NOT_CONNECTED: 'E_NOT_CONNECTED',
|
||||
E_FAN: 'E_FAN',
|
||||
E_LAMP: 'E_LAMP',
|
||||
E_TEMP: 'E_TEMP',
|
||||
E_COVER: 'E_COVER',
|
||||
E_FILTER: 'E_FILTER',
|
||||
E_AUTHENTICATION: 'E_AUTHENTICATION',
|
||||
E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
|
||||
E_UNDEFINED: 'E_UNDEFINED',
|
||||
E_PARAMETER: 'E_PARAMETER',
|
||||
E_UNAVAILABLE: 'E_UNAVAILABLE',
|
||||
E_PROJECTOR: 'E_PROJECTOR',
|
||||
E_INVALID_DATA: 'E_INVALID_DATA',
|
||||
E_WARN: 'E_WARN',
|
||||
E_ERROR: 'E_ERROR',
|
||||
E_CLASS: 'E_CLASS',
|
||||
E_PREFIX: 'E_PREFIX', # Last projector error
|
||||
E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED', # First QtSocket error
|
||||
E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
|
||||
E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
|
||||
E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
|
||||
E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
|
||||
E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
|
||||
E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
|
||||
E_NETWORK: 'E_NETWORK',
|
||||
E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
|
||||
E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
|
||||
E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
|
||||
E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
|
||||
E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
|
||||
E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
|
||||
E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
|
||||
E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
|
||||
E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
|
||||
E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
|
||||
E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
|
||||
E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'}
|
||||
ERROR_STRING = {
|
||||
0: 'S_OK',
|
||||
E_GENERAL: 'E_GENERAL',
|
||||
E_NOT_CONNECTED: 'E_NOT_CONNECTED',
|
||||
E_FAN: 'E_FAN',
|
||||
E_LAMP: 'E_LAMP',
|
||||
E_TEMP: 'E_TEMP',
|
||||
E_COVER: 'E_COVER',
|
||||
E_FILTER: 'E_FILTER',
|
||||
E_AUTHENTICATION: 'E_AUTHENTICATION',
|
||||
E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
|
||||
E_UNDEFINED: 'E_UNDEFINED',
|
||||
E_PARAMETER: 'E_PARAMETER',
|
||||
E_UNAVAILABLE: 'E_UNAVAILABLE',
|
||||
E_PROJECTOR: 'E_PROJECTOR',
|
||||
E_INVALID_DATA: 'E_INVALID_DATA',
|
||||
E_WARN: 'E_WARN',
|
||||
E_ERROR: 'E_ERROR',
|
||||
E_CLASS: 'E_CLASS',
|
||||
E_PREFIX: 'E_PREFIX', # Last projector error
|
||||
E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED', # First QtSocket error
|
||||
E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
|
||||
E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
|
||||
E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
|
||||
E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
|
||||
E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
|
||||
E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
|
||||
E_NETWORK: 'E_NETWORK',
|
||||
E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
|
||||
E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
|
||||
E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
|
||||
E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
|
||||
E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
|
||||
E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
|
||||
E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
|
||||
E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
|
||||
E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
|
||||
E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
|
||||
E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
|
||||
E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'
|
||||
}
|
||||
|
||||
STATUS_STRING = {S_NOT_CONNECTED: 'S_NOT_CONNECTED',
|
||||
S_CONNECTING: 'S_CONNECTING',
|
||||
S_CONNECTED: 'S_CONNECTED',
|
||||
S_STATUS: 'S_STATUS',
|
||||
S_OFF: 'S_OFF',
|
||||
S_INITIALIZE: 'S_INITIALIZE',
|
||||
S_STANDBY: 'S_STANDBY',
|
||||
S_WARMUP: 'S_WARMUP',
|
||||
S_ON: 'S_ON',
|
||||
S_COOLDOWN: 'S_COOLDOWN',
|
||||
S_INFO: 'S_INFO',
|
||||
S_NETWORK_SENDING: 'S_NETWORK_SENDING',
|
||||
S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'}
|
||||
STATUS_STRING = {
|
||||
S_NOT_CONNECTED: 'S_NOT_CONNECTED',
|
||||
S_CONNECTING: 'S_CONNECTING',
|
||||
S_CONNECTED: 'S_CONNECTED',
|
||||
S_STATUS: 'S_STATUS',
|
||||
S_OFF: 'S_OFF',
|
||||
S_INITIALIZE: 'S_INITIALIZE',
|
||||
S_STANDBY: 'S_STANDBY',
|
||||
S_WARMUP: 'S_WARMUP',
|
||||
S_ON: 'S_ON',
|
||||
S_COOLDOWN: 'S_COOLDOWN',
|
||||
S_INFO: 'S_INFO',
|
||||
S_NETWORK_SENDING: 'S_NETWORK_SENDING',
|
||||
S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'
|
||||
}
|
||||
|
||||
# Map error/status codes to message strings
|
||||
ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK
|
||||
E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
|
||||
E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
|
||||
E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
|
||||
E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
|
||||
E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
|
||||
E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
|
||||
E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
|
||||
E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
|
||||
E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
|
||||
E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
|
||||
E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
|
||||
E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
|
||||
E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
|
||||
E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
|
||||
E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
|
||||
E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
|
||||
E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
|
||||
E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
|
||||
'The connection was refused by the peer (or timed out)'),
|
||||
E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
|
||||
'The remote host closed the connection'),
|
||||
E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
|
||||
E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
|
||||
'The socket operation failed because the application '
|
||||
'lacked the required privileges'),
|
||||
E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
|
||||
'The local system ran out of resources (e.g., too many sockets)'),
|
||||
E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
|
||||
'The socket operation timed out'),
|
||||
E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
|
||||
'The datagram was larger than the operating system\'s limit'),
|
||||
E_NETWORK: translate('OpenLP.ProjectorConstants',
|
||||
'An error occurred with the network (Possibly someone pulled the plug?)'),
|
||||
E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
|
||||
'The address specified with socket.bind() '
|
||||
'is already in use and was set to be exclusive'),
|
||||
E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
|
||||
'The address specified to socket.bind() '
|
||||
'does not belong to the host'),
|
||||
E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
|
||||
'The requested socket operation is not supported by the local '
|
||||
'operating system (e.g., lack of IPv6 support)'),
|
||||
E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
|
||||
'The socket is using a proxy, '
|
||||
'and the proxy requires authentication'),
|
||||
E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
|
||||
'The SSL/TLS handshake failed'),
|
||||
E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
|
||||
'The last operation attempted has not finished yet '
|
||||
'(still in progress in the background)'),
|
||||
E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
|
||||
'Could not contact the proxy server because the connection '
|
||||
'to that server was denied'),
|
||||
E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
|
||||
'The connection to the proxy server was closed unexpectedly '
|
||||
'(before the connection to the final peer was established)'),
|
||||
E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
|
||||
'The connection to the proxy server timed out or the proxy '
|
||||
'server stopped responding in the authentication phase.'),
|
||||
E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
|
||||
'The proxy address set with setProxy() was not found'),
|
||||
E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
|
||||
'The connection negotiation with the proxy server failed because the '
|
||||
'response from the proxy server could not be understood'),
|
||||
E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
|
||||
S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
|
||||
S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
|
||||
S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
|
||||
S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
|
||||
S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
|
||||
S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
|
||||
S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
|
||||
S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
|
||||
S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
|
||||
S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
|
||||
S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
|
||||
S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
|
||||
S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')}
|
||||
ERROR_MSG = {
|
||||
E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK
|
||||
E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
|
||||
E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
|
||||
E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
|
||||
E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
|
||||
E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
|
||||
E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
|
||||
E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
|
||||
E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
|
||||
E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
|
||||
E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
|
||||
E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
|
||||
E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
|
||||
E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
|
||||
E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
|
||||
E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
|
||||
E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
|
||||
E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
|
||||
E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
|
||||
'The connection was refused by the peer (or timed out)'),
|
||||
E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
|
||||
'The remote host closed the connection'),
|
||||
E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
|
||||
E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
|
||||
'The socket operation failed because the application '
|
||||
'lacked the required privileges'),
|
||||
E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
|
||||
'The local system ran out of resources (e.g., too many sockets)'),
|
||||
E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
|
||||
'The socket operation timed out'),
|
||||
E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
|
||||
'The datagram was larger than the operating system\'s limit'),
|
||||
E_NETWORK: translate('OpenLP.ProjectorConstants',
|
||||
'An error occurred with the network (Possibly someone pulled the plug?)'),
|
||||
E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
|
||||
'The address specified with socket.bind() '
|
||||
'is already in use and was set to be exclusive'),
|
||||
E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
|
||||
'The address specified to socket.bind() '
|
||||
'does not belong to the host'),
|
||||
E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
|
||||
'The requested socket operation is not supported by the local '
|
||||
'operating system (e.g., lack of IPv6 support)'),
|
||||
E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
|
||||
'The socket is using a proxy, '
|
||||
'and the proxy requires authentication'),
|
||||
E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
|
||||
'The SSL/TLS handshake failed'),
|
||||
E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
|
||||
'The last operation attempted has not finished yet '
|
||||
'(still in progress in the background)'),
|
||||
E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
|
||||
'Could not contact the proxy server because the connection '
|
||||
'to that server was denied'),
|
||||
E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
|
||||
'The connection to the proxy server was closed unexpectedly '
|
||||
'(before the connection to the final peer was established)'),
|
||||
E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
|
||||
'The connection to the proxy server timed out or the proxy '
|
||||
'server stopped responding in the authentication phase.'),
|
||||
E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
|
||||
'The proxy address set with setProxy() was not found'),
|
||||
E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
|
||||
'The connection negotiation with the proxy server failed because the '
|
||||
'response from the proxy server could not be understood'),
|
||||
E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
|
||||
S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
|
||||
S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
|
||||
S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
|
||||
S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
|
||||
S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
|
||||
S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
|
||||
S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
|
||||
S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
|
||||
S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
|
||||
S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
|
||||
S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
|
||||
S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
|
||||
S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')
|
||||
}
|
||||
|
||||
# Map for ERST return codes to string
|
||||
PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK],
|
||||
'1': ERROR_STRING[E_WARN],
|
||||
'2': ERROR_STRING[E_ERROR]}
|
||||
PJLINK_ERST_STATUS = {
|
||||
'0': ERROR_STRING[E_OK],
|
||||
'1': ERROR_STRING[E_WARN],
|
||||
'2': ERROR_STRING[E_ERROR]
|
||||
}
|
||||
|
||||
# Map for POWR return codes to status code
|
||||
PJLINK_POWR_STATUS = {'0': S_STANDBY,
|
||||
'1': S_ON,
|
||||
'2': S_COOLDOWN,
|
||||
'3': S_WARMUP,
|
||||
S_STANDBY: '0',
|
||||
S_ON: '1',
|
||||
S_COOLDOWN: '2',
|
||||
S_WARMUP: '3'}
|
||||
PJLINK_POWR_STATUS = {
|
||||
'0': S_STANDBY,
|
||||
'1': S_ON,
|
||||
'2': S_COOLDOWN,
|
||||
'3': S_WARMUP,
|
||||
S_STANDBY: '0',
|
||||
S_ON: '1',
|
||||
S_COOLDOWN: '2',
|
||||
S_WARMUP: '3'
|
||||
}
|
||||
|
||||
PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'),
|
||||
'2': translate('OpenLP.DB', 'Video'),
|
||||
'3': translate('OpenLP.DB', 'Digital'),
|
||||
'4': translate('OpenLP.DB', 'Storage'),
|
||||
'5': translate('OpenLP.DB', 'Network')}
|
||||
PJLINK_DEFAULT_SOURCES = {
|
||||
'1': translate('OpenLP.DB', 'RGB'),
|
||||
'2': translate('OpenLP.DB', 'Video'),
|
||||
'3': translate('OpenLP.DB', 'Digital'),
|
||||
'4': translate('OpenLP.DB', 'Storage'),
|
||||
'5': translate('OpenLP.DB', 'Network')
|
||||
}
|
||||
|
||||
PJLINK_DEFAULT_CODES = {'11': translate('OpenLP.DB', 'RGB 1'),
|
||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
||||
'21': translate('OpenLP.DB', 'Video 1'),
|
||||
'22': translate('OpenLP.DB', 'Video 2'),
|
||||
'23': translate('OpenLP.DB', 'Video 3'),
|
||||
'24': translate('OpenLP.DB', 'Video 4'),
|
||||
'25': translate('OpenLP.DB', 'Video 5'),
|
||||
'26': translate('OpenLP.DB', 'Video 6'),
|
||||
'27': translate('OpenLP.DB', 'Video 7'),
|
||||
'28': translate('OpenLP.DB', 'Video 8'),
|
||||
'29': translate('OpenLP.DB', 'Video 9'),
|
||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
||||
'49': translate('OpenLP.DB', 'Storage 9'),
|
||||
'51': translate('OpenLP.DB', 'Network 1'),
|
||||
'52': translate('OpenLP.DB', 'Network 2'),
|
||||
'53': translate('OpenLP.DB', 'Network 3'),
|
||||
'54': translate('OpenLP.DB', 'Network 4'),
|
||||
'55': translate('OpenLP.DB', 'Network 5'),
|
||||
'56': translate('OpenLP.DB', 'Network 6'),
|
||||
'57': translate('OpenLP.DB', 'Network 7'),
|
||||
'58': translate('OpenLP.DB', 'Network 8'),
|
||||
'59': translate('OpenLP.DB', 'Network 9')
|
||||
}
|
||||
PJLINK_DEFAULT_CODES = {
|
||||
'11': translate('OpenLP.DB', 'RGB 1'),
|
||||
'12': translate('OpenLP.DB', 'RGB 2'),
|
||||
'13': translate('OpenLP.DB', 'RGB 3'),
|
||||
'14': translate('OpenLP.DB', 'RGB 4'),
|
||||
'15': translate('OpenLP.DB', 'RGB 5'),
|
||||
'16': translate('OpenLP.DB', 'RGB 6'),
|
||||
'17': translate('OpenLP.DB', 'RGB 7'),
|
||||
'18': translate('OpenLP.DB', 'RGB 8'),
|
||||
'19': translate('OpenLP.DB', 'RGB 9'),
|
||||
'21': translate('OpenLP.DB', 'Video 1'),
|
||||
'22': translate('OpenLP.DB', 'Video 2'),
|
||||
'23': translate('OpenLP.DB', 'Video 3'),
|
||||
'24': translate('OpenLP.DB', 'Video 4'),
|
||||
'25': translate('OpenLP.DB', 'Video 5'),
|
||||
'26': translate('OpenLP.DB', 'Video 6'),
|
||||
'27': translate('OpenLP.DB', 'Video 7'),
|
||||
'28': translate('OpenLP.DB', 'Video 8'),
|
||||
'29': translate('OpenLP.DB', 'Video 9'),
|
||||
'31': translate('OpenLP.DB', 'Digital 1'),
|
||||
'32': translate('OpenLP.DB', 'Digital 2'),
|
||||
'33': translate('OpenLP.DB', 'Digital 3'),
|
||||
'34': translate('OpenLP.DB', 'Digital 4'),
|
||||
'35': translate('OpenLP.DB', 'Digital 5'),
|
||||
'36': translate('OpenLP.DB', 'Digital 6'),
|
||||
'37': translate('OpenLP.DB', 'Digital 7'),
|
||||
'38': translate('OpenLP.DB', 'Digital 8'),
|
||||
'39': translate('OpenLP.DB', 'Digital 9'),
|
||||
'41': translate('OpenLP.DB', 'Storage 1'),
|
||||
'42': translate('OpenLP.DB', 'Storage 2'),
|
||||
'43': translate('OpenLP.DB', 'Storage 3'),
|
||||
'44': translate('OpenLP.DB', 'Storage 4'),
|
||||
'45': translate('OpenLP.DB', 'Storage 5'),
|
||||
'46': translate('OpenLP.DB', 'Storage 6'),
|
||||
'47': translate('OpenLP.DB', 'Storage 7'),
|
||||
'48': translate('OpenLP.DB', 'Storage 8'),
|
||||
'49': translate('OpenLP.DB', 'Storage 9'),
|
||||
'51': translate('OpenLP.DB', 'Network 1'),
|
||||
'52': translate('OpenLP.DB', 'Network 2'),
|
||||
'53': translate('OpenLP.DB', 'Network 3'),
|
||||
'54': translate('OpenLP.DB', 'Network 4'),
|
||||
'55': translate('OpenLP.DB', 'Network 5'),
|
||||
'56': translate('OpenLP.DB', 'Network 6'),
|
||||
'57': translate('OpenLP.DB', 'Network 7'),
|
||||
'58': translate('OpenLP.DB', 'Network 8'),
|
||||
'59': translate('OpenLP.DB', 'Network 9')
|
||||
}
|
||||
|
@ -46,15 +46,19 @@ __all__ = ['PJLink1']
|
||||
|
||||
from codecs import decode
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
|
||||
from PyQt5 import QtCore, QtNetwork
|
||||
|
||||
from openlp.core.common import translate, qmd5_hash
|
||||
from openlp.core.lib.projector.constants import *
|
||||
from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \
|
||||
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
|
||||
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
|
||||
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
|
||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \
|
||||
S_OFF, S_OK, S_ON, S_STATUS
|
||||
|
||||
# Shortcuts
|
||||
SocketError = QAbstractSocket.SocketError
|
||||
SocketSTate = QAbstractSocket.SocketState
|
||||
SocketError = QtNetwork.QAbstractSocket.SocketError
|
||||
SocketSTate = QtNetwork.QAbstractSocket.SocketState
|
||||
|
||||
PJLINK_PREFIX = '%'
|
||||
PJLINK_CLASS = '1'
|
||||
@ -62,18 +66,18 @@ PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJL
|
||||
PJLINK_SUFFIX = CR
|
||||
|
||||
|
||||
class PJLink1(QTcpSocket):
|
||||
class PJLink1(QtNetwork.QTcpSocket):
|
||||
"""
|
||||
Socket service for connecting to a PJLink-capable projector.
|
||||
"""
|
||||
# Signals sent by this module
|
||||
changeStatus = pyqtSignal(str, int, str)
|
||||
projectorNetwork = pyqtSignal(int) # Projector network activity
|
||||
projectorStatus = pyqtSignal(int) # Status update
|
||||
projectorAuthentication = pyqtSignal(str) # Authentication error
|
||||
projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed
|
||||
projectorReceivedData = pyqtSignal() # Notify when received data finished processing
|
||||
projectorUpdateIcons = pyqtSignal() # Update the status icons on toolbar
|
||||
changeStatus = QtCore.pyqtSignal(str, int, str)
|
||||
projectorNetwork = QtCore.pyqtSignal(int) # Projector network activity
|
||||
projectorStatus = QtCore.pyqtSignal(int) # Status update
|
||||
projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error
|
||||
projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
|
||||
projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
|
||||
projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
|
||||
|
||||
def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
|
||||
"""
|
||||
@ -116,8 +120,8 @@ class PJLink1(QTcpSocket):
|
||||
self.error_status = S_OK
|
||||
# Socket information
|
||||
# Add enough space to input buffer for extraneous \n \r
|
||||
self.maxSize = PJLINK_MAX_PACKET + 2
|
||||
self.setReadBufferSize(self.maxSize)
|
||||
self.max_size = PJLINK_MAX_PACKET + 2
|
||||
self.setReadBufferSize(self.max_size)
|
||||
# PJLink information
|
||||
self.pjlink_class = '1' # Default class
|
||||
self.reset_information()
|
||||
@ -129,19 +133,20 @@ class PJLink1(QTcpSocket):
|
||||
# Socket timer for some possible brain-dead projectors or network cable pulled
|
||||
self.socket_timer = None
|
||||
# Map command to function
|
||||
self.PJLINK1_FUNC = {'AVMT': self.process_avmt,
|
||||
'CLSS': self.process_clss,
|
||||
'ERST': self.process_erst,
|
||||
'INFO': self.process_info,
|
||||
'INF1': self.process_inf1,
|
||||
'INF2': self.process_inf2,
|
||||
'INPT': self.process_inpt,
|
||||
'INST': self.process_inst,
|
||||
'LAMP': self.process_lamp,
|
||||
'NAME': self.process_name,
|
||||
'PJLINK': self.check_login,
|
||||
'POWR': self.process_powr
|
||||
}
|
||||
self.pjlink1_functions = {
|
||||
'AVMT': self.process_avmt,
|
||||
'CLSS': self.process_clss,
|
||||
'ERST': self.process_erst,
|
||||
'INFO': self.process_info,
|
||||
'INF1': self.process_inf1,
|
||||
'INF2': self.process_inf2,
|
||||
'INPT': self.process_inpt,
|
||||
'INST': self.process_inst,
|
||||
'LAMP': self.process_lamp,
|
||||
'NAME': self.process_name,
|
||||
'PJLINK': self.check_login,
|
||||
'POWR': self.process_powr
|
||||
}
|
||||
|
||||
def reset_information(self):
|
||||
"""
|
||||
@ -291,7 +296,7 @@ class PJLink1(QTcpSocket):
|
||||
message=status_message if msg is None else msg))
|
||||
self.changeStatus.emit(self.ip, status, message)
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def check_login(self, data=None):
|
||||
"""
|
||||
Processes the initial connection and authentication (if needed).
|
||||
@ -309,8 +314,8 @@ class PJLink1(QTcpSocket):
|
||||
log.error('({ip}) Socket timeout waiting for login'.format(ip=self.ip))
|
||||
self.change_status(E_SOCKET_TIMEOUT)
|
||||
return
|
||||
read = self.readLine(self.maxSize)
|
||||
dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n
|
||||
read = self.readLine(self.max_size)
|
||||
dontcare = self.readLine(self.max_size) # Clean out the trailing \r\n
|
||||
if read is None:
|
||||
log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
|
||||
return
|
||||
@ -320,7 +325,7 @@ class PJLink1(QTcpSocket):
|
||||
data = decode(read, 'ascii')
|
||||
# Possibility of extraneous data on input when reading.
|
||||
# Clean out extraneous characters in buffer.
|
||||
dontcare = self.readLine(self.maxSize)
|
||||
dontcare = self.readLine(self.max_size)
|
||||
log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip()))
|
||||
# At this point, we should only have the initial login prompt with
|
||||
# possible authentication
|
||||
@ -379,7 +384,7 @@ class PJLink1(QTcpSocket):
|
||||
self.timer.setInterval(2000) # Set 2 seconds for initial information
|
||||
self.timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def get_data(self):
|
||||
"""
|
||||
Socket interface to retrieve data.
|
||||
@ -389,7 +394,7 @@ class PJLink1(QTcpSocket):
|
||||
log.debug('({ip}) get_data(): Not connected - returning'.format(ip=self.ip))
|
||||
self.send_busy = False
|
||||
return
|
||||
read = self.readLine(self.maxSize)
|
||||
read = self.readLine(self.max_size)
|
||||
if read == -1:
|
||||
# No data available
|
||||
log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip))
|
||||
@ -436,7 +441,7 @@ class PJLink1(QTcpSocket):
|
||||
return
|
||||
return self.process_command(cmd, data)
|
||||
|
||||
@pyqtSlot(QAbstractSocket.SocketError)
|
||||
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
|
||||
def get_error(self, err):
|
||||
"""
|
||||
Process error from SocketError signal.
|
||||
@ -504,7 +509,7 @@ class PJLink1(QTcpSocket):
|
||||
log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip))
|
||||
self._send_command()
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def _send_command(self, data=None):
|
||||
"""
|
||||
Socket interface to send data. If data=None, then check queue.
|
||||
@ -586,8 +591,8 @@ class PJLink1(QTcpSocket):
|
||||
self.projectorReceivedData.emit()
|
||||
return
|
||||
|
||||
if cmd in self.PJLINK1_FUNC:
|
||||
self.PJLINK1_FUNC[cmd](data)
|
||||
if cmd in self.pjlink1_functions:
|
||||
self.pjlink1_functions[cmd](data)
|
||||
else:
|
||||
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
||||
self.send_busy = False
|
||||
@ -812,9 +817,9 @@ class PJLink1(QTcpSocket):
|
||||
log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
|
||||
return
|
||||
self.change_status(S_CONNECTING)
|
||||
self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port))
|
||||
self.connectToHost(self.ip, self.port if isinstance(self.port, int) else int(self.port))
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def disconnect_from_host(self, abort=False):
|
||||
"""
|
||||
Close socket and cleanup.
|
||||
|
@ -257,7 +257,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.data_directory_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Data Location'))
|
||||
self.recent_label.setText(translate('OpenLP.AdvancedTab', 'Number of recent service files to display:'))
|
||||
self.media_plugin_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Open the last used Library category on startup'))
|
||||
'Open the last used Library tab on startup'))
|
||||
self.double_click_live_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Double-click to send items straight to Live'))
|
||||
self.single_click_preview_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
@ -265,7 +265,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.single_click_service_preview_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Preview items when clicked in Service'))
|
||||
self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab',
|
||||
'Expand new Service items on creation'))
|
||||
'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_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Disabled'))
|
||||
|
@ -106,7 +106,7 @@ class Ui_ExceptionDialog(object):
|
||||
translate('OpenLP.ExceptionDialog', '{first_part}'
|
||||
'<strong>No email app? </strong> You can <strong>save</strong> this '
|
||||
'information to a <strong>file</strong> and<br>'
|
||||
'send it from your <strong>mail on browser</strong> via an <strong>attachement.</strong><br><br>'
|
||||
'send it from your <strong>mail on browser</strong> via an <strong>attachment.</strong><br><br>'
|
||||
'<strong>Thank you<strong> for being part of making OpenLP better!<br>'
|
||||
).format(first_part=exception_part1))
|
||||
self.send_report_button.setText(translate('OpenLP.ExceptionDialog', 'Send E-Mail'))
|
||||
|
@ -197,13 +197,18 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
||||
"""
|
||||
count = int(20 - len(self.description_text_edit.toPlainText()))
|
||||
if count < 0:
|
||||
count = 0
|
||||
self.__button_state(True)
|
||||
self.description_word_count.setText(
|
||||
translate('OpenLP.ExceptionDialog', '<strong>Thank you for your description!</strong>'))
|
||||
elif count == 20:
|
||||
self.__button_state(False)
|
||||
self.description_word_count.setText(
|
||||
translate('OpenLP.ExceptionDialog', '<strong>Tell us what you were doing when this happened.</strong>'))
|
||||
else:
|
||||
self.__button_state(False)
|
||||
self.description_word_count.setText(
|
||||
translate('OpenLP.ExceptionDialog', '{count} characters remaining from the minimum description.'
|
||||
).format(count=count))
|
||||
self.description_word_count.setText(
|
||||
translate('OpenLP.ExceptionDialog', '<strong>Please enter a more detailed description of the situation'
|
||||
))
|
||||
|
||||
def on_attach_file_button_clicked(self):
|
||||
"""
|
||||
|
@ -429,13 +429,13 @@ class Ui_MainWindow(object):
|
||||
translate('OpenLP.MainWindow', 'Export settings to a *.config file.'))
|
||||
self.settings_export_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
||||
self.settings_import_item.setStatusTip(
|
||||
translate('OpenLP.MainWindow', 'Import OpenLP settings from a *.config file previously exported from '
|
||||
'this or an another machine.'))
|
||||
translate('OpenLP.MainWindow', 'Import settings from a *.config file previously exported from '
|
||||
'this or another machine.'))
|
||||
self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
||||
self.view_projector_manager_item.setText(translate('OPenLP.MainWindow', '&Projectors'))
|
||||
self.view_projector_manager_item.setText(translate('OpenLP.MainWindow', '&Projectors'))
|
||||
self.view_projector_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Projectors.'))
|
||||
self.view_projector_manager_item.setStatusTip(translate('OpenLP.MainWindow',
|
||||
'Toggle the visibility of the Projectors.'))
|
||||
'Toggle visibility of the Projectors.'))
|
||||
self.view_media_manager_item.setText(translate('OpenLP.MainWindow', 'L&ibrary'))
|
||||
self.view_media_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show the Library.'))
|
||||
self.view_media_manager_item.setStatusTip(translate('OpenLP.MainWindow',
|
||||
@ -443,22 +443,22 @@ class Ui_MainWindow(object):
|
||||
self.view_theme_manager_item.setText(translate('OpenLP.MainWindow', '&Themes'))
|
||||
self.view_theme_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show themes'))
|
||||
self.view_theme_manager_item.setStatusTip(translate('OpenLP.MainWindow',
|
||||
'Toggle the visibility of the Themes.'))
|
||||
'Toggle visibility of the Themes.'))
|
||||
self.view_service_manager_item.setText(translate('OpenLP.MainWindow', '&Service'))
|
||||
self.view_service_manager_item.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Service.'))
|
||||
self.view_service_manager_item.setStatusTip(translate('OpenLP.MainWindow',
|
||||
'Toggle the visibility of the Service.'))
|
||||
'Toggle visibility of the Service.'))
|
||||
self.view_preview_panel.setText(translate('OpenLP.MainWindow', '&Preview'))
|
||||
self.view_preview_panel.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Preview.'))
|
||||
self.view_preview_panel.setStatusTip(
|
||||
translate('OpenLP.MainWindow', 'Toggle the visibility of the Preview.'))
|
||||
translate('OpenLP.MainWindow', 'Toggle visibility of the Preview.'))
|
||||
self.view_live_panel.setText(translate('OpenLP.MainWindow', 'Li&ve'))
|
||||
self.view_live_panel.setToolTip(translate('OpenLP.MainWindow', 'Hide or show Live'))
|
||||
self.lock_panel.setText(translate('OpenLP.MainWindow', 'L&ock visibility of the panels'))
|
||||
self.lock_panel.setStatusTip(translate('OpenLP.MainWindow', 'Lock visibility of the panels.'))
|
||||
self.view_live_panel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the Live.'))
|
||||
self.view_live_panel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle visibility of the Live.'))
|
||||
self.settings_plugin_list_item.setText(translate('OpenLP.MainWindow', '&Manage Plugins'))
|
||||
self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'You can activate or disable plugins'
|
||||
self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'You can enable and disable plugins '
|
||||
'from here.'))
|
||||
self.about_item.setText(translate('OpenLP.MainWindow', '&About'))
|
||||
self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP.'))
|
||||
@ -487,9 +487,9 @@ class Ui_MainWindow(object):
|
||||
self.update_theme_images.setText(translate('OpenLP.MainWindow', 'Update Theme Images'))
|
||||
self.update_theme_images.setStatusTip(translate('OpenLP.MainWindow',
|
||||
'Update the preview images for all themes.'))
|
||||
self.mode_default_item.setText(translate('OpenLP.MainWindow', '&Default'))
|
||||
self.mode_default_item.setStatusTip(translate('OpenLP.MainWindow', 'Reset the interface layout back to the '
|
||||
'default settings.'))
|
||||
self.mode_default_item.setText(translate('OpenLP.MainWindow', '&Show all'))
|
||||
self.mode_default_item.setStatusTip(translate('OpenLP.MainWindow', 'Reset the interface back to the '
|
||||
'default layout and show all the panels.'))
|
||||
self.mode_setup_item.setText(translate('OpenLP.MainWindow', '&Setup'))
|
||||
self.mode_setup_item.setStatusTip(translate('OpenLP.MainWindow', 'Use layout that focuses on setting'
|
||||
' up the Service.'))
|
||||
@ -954,7 +954,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
self,
|
||||
translate('OpenLP.MainWindow', 'Export Settings File'),
|
||||
'',
|
||||
translate('OpenLP.MainWindow', 'Exported OpenLP Settings (*.conf)'))
|
||||
translate('OpenLP.MainWindow', 'OpenLP Settings (*.conf)'))
|
||||
if not export_file_name:
|
||||
return
|
||||
# Make sure it's a .conf file.
|
||||
|
@ -83,17 +83,17 @@ class SystemPlayer(MediaPlayer):
|
||||
elif mime_type.startswith('video/'):
|
||||
self._add_to_list(self.video_extensions_list, mime_type)
|
||||
|
||||
def _add_to_list(self, mime_type_list, mimetype):
|
||||
def _add_to_list(self, mime_type_list, mime_type):
|
||||
"""
|
||||
Add mimetypes to the provided list
|
||||
"""
|
||||
# Add all extensions which mimetypes provides us for supported types.
|
||||
extensions = mimetypes.guess_all_extensions(str(mimetype))
|
||||
extensions = mimetypes.guess_all_extensions(mime_type)
|
||||
for extension in extensions:
|
||||
ext = '*%s' % extension
|
||||
if ext not in mime_type_list:
|
||||
mime_type_list.append(ext)
|
||||
log.info('MediaPlugin: %s extensions: %s' % (mimetype, ' '.join(extensions)))
|
||||
log.info('MediaPlugin: %s extensions: %s', mime_type, ' '.join(extensions))
|
||||
|
||||
def setup(self, display):
|
||||
"""
|
||||
@ -284,25 +284,25 @@ class SystemPlayer(MediaPlayer):
|
||||
:return: True if file can be played otherwise False
|
||||
"""
|
||||
thread = QtCore.QThread()
|
||||
check_media_player = CheckMedia(path)
|
||||
check_media_player.setVolume(0)
|
||||
check_media_player.moveToThread(thread)
|
||||
check_media_player.finished.connect(thread.quit)
|
||||
thread.started.connect(check_media_player.play)
|
||||
check_media_worker = CheckMediaWorker(path)
|
||||
check_media_worker.setVolume(0)
|
||||
check_media_worker.moveToThread(thread)
|
||||
check_media_worker.finished.connect(thread.quit)
|
||||
thread.started.connect(check_media_worker.play)
|
||||
thread.start()
|
||||
while thread.isRunning():
|
||||
self.application.processEvents()
|
||||
return check_media_player.result
|
||||
return check_media_worker.result
|
||||
|
||||
|
||||
class CheckMedia(QtMultimedia.QMediaPlayer):
|
||||
class CheckMediaWorker(QtMultimedia.QMediaPlayer):
|
||||
"""
|
||||
Class used to check if a media file is playable
|
||||
"""
|
||||
finished = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, path):
|
||||
super(CheckMedia, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
|
||||
super(CheckMediaWorker, self).__init__(None, QtMultimedia.QMediaPlayer.VideoSurface)
|
||||
self.result = None
|
||||
|
||||
self.error.connect(functools.partial(self.signals, 'error'))
|
||||
|
@ -30,8 +30,6 @@ log = logging.getLogger(__name__)
|
||||
log.debug('editform loaded')
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtWidgets import QDialog, QPlainTextEdit, QLineEdit, QDialogButtonBox, QLabel, QGridLayout
|
||||
|
||||
from openlp.core.common import translate, verify_ip_address
|
||||
from openlp.core.lib import build_icon
|
||||
@ -53,56 +51,56 @@ class Ui_ProjectorEditForm(object):
|
||||
edit_projector_dialog.setMinimumWidth(400)
|
||||
edit_projector_dialog.setModal(True)
|
||||
# Define the basic layout
|
||||
self.dialog_layout = QGridLayout(edit_projector_dialog)
|
||||
self.dialog_layout = QtWidgets.QGridLayout(edit_projector_dialog)
|
||||
self.dialog_layout.setObjectName('dialog_layout')
|
||||
self.dialog_layout.setSpacing(8)
|
||||
self.dialog_layout.setContentsMargins(8, 8, 8, 8)
|
||||
# IP Address
|
||||
self.ip_label = QLabel(edit_projector_dialog)
|
||||
self.ip_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.ip_label.setObjectName('projector_edit_ip_label')
|
||||
self.ip_text = QLineEdit(edit_projector_dialog)
|
||||
self.ip_text = QtWidgets.QLineEdit(edit_projector_dialog)
|
||||
self.ip_text.setObjectName('projector_edit_ip_text')
|
||||
self.dialog_layout.addWidget(self.ip_label, 0, 0)
|
||||
self.dialog_layout.addWidget(self.ip_text, 0, 1)
|
||||
# Port number
|
||||
self.port_label = QLabel(edit_projector_dialog)
|
||||
self.port_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.port_label.setObjectName('projector_edit_ip_label')
|
||||
self.port_text = QLineEdit(edit_projector_dialog)
|
||||
self.port_text = QtWidgets.QLineEdit(edit_projector_dialog)
|
||||
self.port_text.setObjectName('projector_edit_port_text')
|
||||
self.dialog_layout.addWidget(self.port_label, 1, 0)
|
||||
self.dialog_layout.addWidget(self.port_text, 1, 1)
|
||||
# PIN
|
||||
self.pin_label = QLabel(edit_projector_dialog)
|
||||
self.pin_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.pin_label.setObjectName('projector_edit_pin_label')
|
||||
self.pin_text = QLineEdit(edit_projector_dialog)
|
||||
self.pin_text = QtWidgets.QLineEdit(edit_projector_dialog)
|
||||
self.pin_label.setObjectName('projector_edit_pin_text')
|
||||
self.dialog_layout.addWidget(self.pin_label, 2, 0)
|
||||
self.dialog_layout.addWidget(self.pin_text, 2, 1)
|
||||
# Name
|
||||
self.name_label = QLabel(edit_projector_dialog)
|
||||
self.name_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.name_label.setObjectName('projector_edit_name_label')
|
||||
self.name_text = QLineEdit(edit_projector_dialog)
|
||||
self.name_text = QtWidgets.QLineEdit(edit_projector_dialog)
|
||||
self.name_text.setObjectName('projector_edit_name_text')
|
||||
self.dialog_layout.addWidget(self.name_label, 3, 0)
|
||||
self.dialog_layout.addWidget(self.name_text, 3, 1)
|
||||
# Location
|
||||
self.location_label = QLabel(edit_projector_dialog)
|
||||
self.location_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.location_label.setObjectName('projector_edit_location_label')
|
||||
self.location_text = QLineEdit(edit_projector_dialog)
|
||||
self.location_text = QtWidgets.QLineEdit(edit_projector_dialog)
|
||||
self.location_text.setObjectName('projector_edit_location_text')
|
||||
self.dialog_layout.addWidget(self.location_label, 4, 0)
|
||||
self.dialog_layout.addWidget(self.location_text, 4, 1)
|
||||
# Notes
|
||||
self.notes_label = QLabel(edit_projector_dialog)
|
||||
self.notes_label = QtWidgets.QLabel(edit_projector_dialog)
|
||||
self.notes_label.setObjectName('projector_edit_notes_label')
|
||||
self.notes_text = QPlainTextEdit(edit_projector_dialog)
|
||||
self.notes_text = QtWidgets.QPlainTextEdit(edit_projector_dialog)
|
||||
self.notes_text.setObjectName('projector_edit_notes_text')
|
||||
self.dialog_layout.addWidget(self.notes_label, 5, 0, alignment=QtCore.Qt.AlignTop)
|
||||
self.dialog_layout.addWidget(self.notes_text, 5, 1)
|
||||
# Time for the buttons
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Help |
|
||||
QDialogButtonBox.Save |
|
||||
QDialogButtonBox.Cancel)
|
||||
self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Help |
|
||||
QtWidgets.QDialogButtonBox.Save |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.dialog_layout.addWidget(self.button_box, 8, 0, 1, 2)
|
||||
|
||||
def retranslateUi(self, edit_projector_dialog):
|
||||
@ -128,7 +126,7 @@ class Ui_ProjectorEditForm(object):
|
||||
self.notes_text.insertPlainText(self.projector.notes)
|
||||
|
||||
|
||||
class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
|
||||
class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm):
|
||||
"""
|
||||
Class to add or edit a projector entry in the database.
|
||||
|
||||
@ -140,8 +138,8 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
|
||||
location = Column(String(30))
|
||||
notes = Column(String(200))
|
||||
"""
|
||||
newProjector = pyqtSignal(str)
|
||||
editProjector = pyqtSignal(object)
|
||||
newProjector = QtCore.pyqtSignal(str)
|
||||
editProjector = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None, projectordb=None):
|
||||
super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
|
||||
@ -159,10 +157,10 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
|
||||
self.projector = projector
|
||||
self.new_projector = False
|
||||
self.retranslateUi(self)
|
||||
reply = QDialog.exec(self)
|
||||
reply = QtWidgets.QDialog.exec(self)
|
||||
return reply
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def accept_me(self):
|
||||
"""
|
||||
Validate input before accepting input.
|
||||
@ -247,14 +245,14 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
|
||||
self.editProjector.emit(self.projector)
|
||||
self.close()
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def help_me(self):
|
||||
"""
|
||||
Show a help message about the input fields.
|
||||
"""
|
||||
log.debug('help_me() signal received')
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def cancel_me(self):
|
||||
"""
|
||||
Cancel button clicked - just close.
|
||||
|
@ -26,43 +26,46 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
log.debug('projectormanager loaded')
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QObject, QThread, pyqtSlot
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from openlp.core.common import RegistryProperties, Settings, OpenLPMixin, \
|
||||
RegistryMixin, translate
|
||||
from openlp.core.ui.lib import OpenLPToolbar
|
||||
from openlp.core.lib.ui import create_widget_action
|
||||
from openlp.core.lib.projector import DialogSourceStyle
|
||||
from openlp.core.lib.projector.constants import *
|
||||
from openlp.core.lib.projector.constants import ERROR_MSG, ERROR_STRING, E_AUTHENTICATION, E_ERROR, \
|
||||
E_NETWORK, E_NOT_CONNECTED, E_UNKNOWN_SOCKET_ERROR, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_COOLDOWN, \
|
||||
S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP
|
||||
from openlp.core.lib.projector.db import ProjectorDB
|
||||
from openlp.core.lib.projector.pjlink1 import PJLink1
|
||||
from openlp.core.ui.projector.editform import ProjectorEditForm
|
||||
from openlp.core.ui.projector.sourceselectform import SourceSelectTabs, SourceSelectSingle
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.debug('projectormanager loaded')
|
||||
|
||||
|
||||
# Dict for matching projector status to display icon
|
||||
STATUS_ICONS = {S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png',
|
||||
S_CONNECTING: ':/projector/projector_item_connect.png',
|
||||
S_CONNECTED: ':/projector/projector_off.png',
|
||||
S_OFF: ':/projector/projector_off.png',
|
||||
S_INITIALIZE: ':/projector/projector_off.png',
|
||||
S_STANDBY: ':/projector/projector_off.png',
|
||||
S_WARMUP: ':/projector/projector_warmup.png',
|
||||
S_ON: ':/projector/projector_on.png',
|
||||
S_COOLDOWN: ':/projector/projector_cooldown.png',
|
||||
E_ERROR: ':/projector/projector_error.png',
|
||||
E_NETWORK: ':/projector/projector_not_connected_error.png',
|
||||
E_AUTHENTICATION: ':/projector/projector_not_connected_error.png',
|
||||
E_UNKNOWN_SOCKET_ERROR: ':/projector/projector_not_connected_error.png',
|
||||
E_NOT_CONNECTED: ':/projector/projector_not_connected_error.png'
|
||||
}
|
||||
STATUS_ICONS = {
|
||||
S_NOT_CONNECTED: ':/projector/projector_item_disconnect.png',
|
||||
S_CONNECTING: ':/projector/projector_item_connect.png',
|
||||
S_CONNECTED: ':/projector/projector_off.png',
|
||||
S_OFF: ':/projector/projector_off.png',
|
||||
S_INITIALIZE: ':/projector/projector_off.png',
|
||||
S_STANDBY: ':/projector/projector_off.png',
|
||||
S_WARMUP: ':/projector/projector_warmup.png',
|
||||
S_ON: ':/projector/projector_on.png',
|
||||
S_COOLDOWN: ':/projector/projector_cooldown.png',
|
||||
E_ERROR: ':/projector/projector_error.png',
|
||||
E_NETWORK: ':/projector/projector_not_connected_error.png',
|
||||
E_AUTHENTICATION: ':/projector/projector_not_connected_error.png',
|
||||
E_UNKNOWN_SOCKET_ERROR: ':/projector/projector_not_connected_error.png',
|
||||
E_NOT_CONNECTED: ':/projector/projector_not_connected_error.png'
|
||||
}
|
||||
|
||||
|
||||
class Ui_ProjectorManager(object):
|
||||
class UiProjectorManager(object):
|
||||
"""
|
||||
UI part of the Projector Manager
|
||||
"""
|
||||
@ -271,7 +274,7 @@ class Ui_ProjectorManager(object):
|
||||
self.update_icons()
|
||||
|
||||
|
||||
class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager, RegistryProperties):
|
||||
class ProjectorManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, UiProjectorManager, RegistryProperties):
|
||||
"""
|
||||
Manage the projectors.
|
||||
"""
|
||||
@ -720,7 +723,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
widget.setData(QtCore.Qt.UserRole, item)
|
||||
item.link.db_item = item.db_item
|
||||
item.widget = widget
|
||||
thread = QThread(parent=self)
|
||||
thread = QtCore.QThread(parent=self)
|
||||
thread.my_parent = self
|
||||
item.moveToThread(thread)
|
||||
thread.started.connect(item.link.thread_started)
|
||||
@ -751,7 +754,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
for item in self.projector_list:
|
||||
log.debug('New projector list - item: ({ip}) {name}'.format(ip=item.link.ip, name=item.link.name))
|
||||
|
||||
@pyqtSlot(str)
|
||||
@QtCore.pyqtSlot(str)
|
||||
def add_projector_from_wizard(self, ip, opts=None):
|
||||
"""
|
||||
Add a projector from the edit dialog
|
||||
@ -763,7 +766,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
item = self.projectordb.get_projector_by_ip(ip)
|
||||
self.add_projector(item)
|
||||
|
||||
@pyqtSlot(object)
|
||||
@QtCore.pyqtSlot(object)
|
||||
def edit_projector_from_wizard(self, projector):
|
||||
"""
|
||||
Update projector from the wizard edit page
|
||||
@ -796,7 +799,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
"""
|
||||
return self.projector_list
|
||||
|
||||
@pyqtSlot(str, int, str)
|
||||
@QtCore.pyqtSlot(str, int, str)
|
||||
def update_status(self, ip, status=None, msg=None):
|
||||
"""
|
||||
Update the status information/icon for selected list item
|
||||
@ -846,7 +849,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
item.setVisible(False if hidden else True)
|
||||
item.setEnabled(True if enabled else False)
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def update_icons(self):
|
||||
"""
|
||||
Update the icons when the selected projectors change
|
||||
@ -919,7 +922,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
self.get_toolbar_item('blank_projector_multiple', hidden=False, enabled=True)
|
||||
self.get_toolbar_item('show_projector_multiple', hidden=False, enabled=True)
|
||||
|
||||
@pyqtSlot(str)
|
||||
@QtCore.pyqtSlot(str)
|
||||
def authentication_error(self, name):
|
||||
"""
|
||||
Display warning dialog when attempting to connect with invalid pin
|
||||
@ -933,7 +936,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
'<br /><br />Please verify your PIN setting '
|
||||
'for projector item "{name}"'.format(name=name))
|
||||
|
||||
@pyqtSlot(str)
|
||||
@QtCore.pyqtSlot(str)
|
||||
def no_authentication_error(self, name):
|
||||
"""
|
||||
Display warning dialog when pin saved for item but projector does not
|
||||
@ -949,7 +952,7 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
'for projector item "{name}"'.format(name=name))
|
||||
|
||||
|
||||
class ProjectorItem(QObject):
|
||||
class ProjectorItem(QtCore.QObject):
|
||||
"""
|
||||
Class for the projector list widget item.
|
||||
NOTE: Actual PJLink class instance should be saved as self.link
|
||||
|
@ -20,24 +20,21 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
:mod: `openlp.core.ui.projector.sourceselectform` module
|
||||
:mod: `openlp.core.ui.projector.sourceselectform` module
|
||||
|
||||
Provides the dialog window for selecting video source for projector.
|
||||
Provides the dialog window for selecting video source for projector.
|
||||
"""
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
log.debug('editform loaded')
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtCore import pyqtSlot, QSize
|
||||
from PyQt5.QtWidgets import QAbstractButton, QDialog, QButtonGroup, QDialogButtonBox, QFormLayout, QLineEdit, \
|
||||
QRadioButton, QStyle, QStylePainter, QStyleOptionTab, QTabBar, QTabWidget, QVBoxLayout, QWidget
|
||||
|
||||
from openlp.core.common import translate, is_macosx
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.projector.db import ProjectorSource
|
||||
from openlp.core.lib.projector.constants import PJLINK_DEFAULT_SOURCES, PJLINK_DEFAULT_CODES
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def source_group(inputs, source_text):
|
||||
"""
|
||||
@ -78,7 +75,7 @@ def source_group(inputs, source_text):
|
||||
return keydict
|
||||
|
||||
|
||||
def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
def build_tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
"""
|
||||
Create the radio button page for a tab.
|
||||
Dictionary will be a 1-key entry where key=tab to setup, val=list of inputs.
|
||||
@ -104,8 +101,8 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
:param edit: If we're editing the source text
|
||||
"""
|
||||
buttonchecked = False
|
||||
widget = QWidget()
|
||||
layout = QFormLayout() if edit else QVBoxLayout()
|
||||
widget = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QFormLayout() if edit else QtWidgets.QVBoxLayout()
|
||||
layout.setSpacing(10)
|
||||
widget.setLayout(layout)
|
||||
tempkey = list(source_key.keys())[0] # Should only be 1 key
|
||||
@ -114,7 +111,7 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
button_count = len(sourcelist)
|
||||
if edit:
|
||||
for key in sourcelist:
|
||||
item = QLineEdit()
|
||||
item = QtWidgets.QLineEdit()
|
||||
item.setObjectName('source_key_{key}'.format(key=key))
|
||||
source_item = projectordb.get_source_by_code(code=key, projector_id=projector.db_item.id)
|
||||
if source_item is None:
|
||||
@ -130,7 +127,7 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
text = source_key[tempkey][key]
|
||||
else:
|
||||
text = source_item.text
|
||||
itemwidget = QRadioButton(text)
|
||||
itemwidget = QtWidgets.QRadioButton(text)
|
||||
itemwidget.setAutoExclusive(True)
|
||||
if default == key:
|
||||
itemwidget.setChecked(True)
|
||||
@ -148,23 +145,23 @@ def set_button_tooltip(button_bar):
|
||||
:param button_bar: QDialogButtonBar instance to update
|
||||
"""
|
||||
for button in button_bar.buttons():
|
||||
if button_bar.standardButton(button) == QDialogButtonBox.Cancel:
|
||||
if button_bar.standardButton(button) == QtWidgets.QDialogButtonBox.Cancel:
|
||||
button.setToolTip(translate('OpenLP.SourceSelectForm',
|
||||
'Ignoring current changes and return to OpenLP'))
|
||||
elif button_bar.standardButton(button) == QDialogButtonBox.Reset:
|
||||
elif button_bar.standardButton(button) == QtWidgets.QDialogButtonBox.Reset:
|
||||
button.setToolTip(translate('OpenLP.SourceSelectForm',
|
||||
'Delete all user-defined text and revert to PJLink default text'))
|
||||
elif button_bar.standardButton(button) == QDialogButtonBox.Discard:
|
||||
elif button_bar.standardButton(button) == QtWidgets.QDialogButtonBox.Discard:
|
||||
button.setToolTip(translate('OpenLP.SourceSelectForm',
|
||||
'Discard changes and reset to previous user-defined text'))
|
||||
elif button_bar.standardButton(button) == QDialogButtonBox.Ok:
|
||||
elif button_bar.standardButton(button) == QtWidgets.QDialogButtonBox.Ok:
|
||||
button.setToolTip(translate('OpenLP.SourceSelectForm',
|
||||
'Save changes and return to OpenLP'))
|
||||
else:
|
||||
log.debug('No tooltip for button {text}'.format(text=button.text()))
|
||||
|
||||
|
||||
class FingerTabBarWidget(QTabBar):
|
||||
class FingerTabBarWidget(QtWidgets.QTabBar):
|
||||
"""
|
||||
Realign west -orientation tabs to left-right text rather than south-north text
|
||||
Borrowed from
|
||||
@ -177,8 +174,8 @@ class FingerTabBarWidget(QTabBar):
|
||||
:param width: Remove default width parameter in kwargs
|
||||
:param height: Remove default height parameter in kwargs
|
||||
"""
|
||||
self.tabSize = QSize(kwargs.pop('width', 100), kwargs.pop('height', 25))
|
||||
QTabBar.__init__(self, parent, *args, **kwargs)
|
||||
self.tabSize = QtCore.QSize(kwargs.pop('width', 100), kwargs.pop('height', 25))
|
||||
QtWidgets.QTabBar.__init__(self, parent, *args, **kwargs)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""
|
||||
@ -186,14 +183,14 @@ class FingerTabBarWidget(QTabBar):
|
||||
|
||||
:param event: Repaint event signal
|
||||
"""
|
||||
painter = QStylePainter(self)
|
||||
option = QStyleOptionTab()
|
||||
painter = QtWidgets.QStylePainter(self)
|
||||
option = QtWidgets.QStyleOptionTab()
|
||||
|
||||
for index in range(self.count()):
|
||||
self.initStyleOption(option, index)
|
||||
tabRect = self.tabRect(index)
|
||||
tabRect.moveLeft(10)
|
||||
painter.drawControl(QStyle.CE_TabBarTabShape, option)
|
||||
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
|
||||
painter.drawText(tabRect, QtCore.Qt.AlignVCenter |
|
||||
QtCore.Qt.TextDontClip,
|
||||
self.tabText(index))
|
||||
@ -209,7 +206,7 @@ class FingerTabBarWidget(QTabBar):
|
||||
return self.tabSize
|
||||
|
||||
|
||||
class FingerTabWidget(QTabWidget):
|
||||
class FingerTabWidget(QtWidgets.QTabWidget):
|
||||
"""
|
||||
A QTabWidget equivalent which uses our FingerTabBarWidget
|
||||
|
||||
@ -220,11 +217,11 @@ class FingerTabWidget(QTabWidget):
|
||||
"""
|
||||
Initialize FingerTabWidget instance
|
||||
"""
|
||||
QTabWidget.__init__(self, parent, *args)
|
||||
QtWidgets.QTabWidget.__init__(self, parent, *args)
|
||||
self.setTabBar(FingerTabBarWidget(self))
|
||||
|
||||
|
||||
class SourceSelectTabs(QDialog):
|
||||
class SourceSelectTabs(QtWidgets.QDialog):
|
||||
"""
|
||||
Class for handling selecting the source for the projector to use.
|
||||
Uses tabbed interface.
|
||||
@ -248,18 +245,18 @@ class SourceSelectTabs(QDialog):
|
||||
self.setObjectName('source_select_tabs')
|
||||
self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png'))
|
||||
self.setModal(True)
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout = QtWidgets.QVBoxLayout()
|
||||
self.layout.setObjectName('source_select_tabs_layout')
|
||||
if is_macosx():
|
||||
self.tabwidget = QTabWidget(self)
|
||||
self.tabwidget = QtWidgets.QTabWidget(self)
|
||||
else:
|
||||
self.tabwidget = FingerTabWidget(self)
|
||||
self.tabwidget.setObjectName('source_select_tabs_tabwidget')
|
||||
self.tabwidget.setUsesScrollButtons(False)
|
||||
if is_macosx():
|
||||
self.tabwidget.setTabPosition(QTabWidget.North)
|
||||
self.tabwidget.setTabPosition(QtWidgets.QTabWidget.North)
|
||||
else:
|
||||
self.tabwidget.setTabPosition(QTabWidget.West)
|
||||
self.tabwidget.setTabPosition(QtWidgets.QTabWidget.West)
|
||||
self.layout.addWidget(self.tabwidget)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
@ -272,13 +269,12 @@ class SourceSelectTabs(QDialog):
|
||||
self.projector = projector
|
||||
self.source_text = self.projectordb.get_source_list(projector=projector)
|
||||
self.source_group = source_group(projector.source_available, self.source_text)
|
||||
# self.source_group = {'4': {'41': 'Storage 1'}, '5': {"51": 'Network 1'}}
|
||||
self.button_group = [] if self.edit else QButtonGroup()
|
||||
self.button_group = [] if self.edit else QtWidgets.QButtonGroup()
|
||||
keys = list(self.source_group.keys())
|
||||
keys.sort()
|
||||
if self.edit:
|
||||
for key in keys:
|
||||
(tab, button_count, buttonchecked) = Build_Tab(group=self.button_group,
|
||||
(tab, button_count, buttonchecked) = build_tab(group=self.button_group,
|
||||
source_key={key: self.source_group[key]},
|
||||
default=self.projector.source,
|
||||
projector=self.projector,
|
||||
@ -287,13 +283,13 @@ class SourceSelectTabs(QDialog):
|
||||
thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key])
|
||||
if buttonchecked:
|
||||
self.tabwidget.setCurrentIndex(thistab)
|
||||
self.button_box = QDialogButtonBox(QtWidgets.QDialogButtonBox.Reset |
|
||||
QtWidgets.QDialogButtonBox.Discard |
|
||||
QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Reset |
|
||||
QtWidgets.QDialogButtonBox.Discard |
|
||||
QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
else:
|
||||
for key in keys:
|
||||
(tab, button_count, buttonchecked) = Build_Tab(group=self.button_group,
|
||||
(tab, button_count, buttonchecked) = build_tab(group=self.button_group,
|
||||
source_key={key: self.source_group[key]},
|
||||
default=self.projector.source,
|
||||
projector=self.projector,
|
||||
@ -302,15 +298,15 @@ class SourceSelectTabs(QDialog):
|
||||
thistab = self.tabwidget.addTab(tab, PJLINK_DEFAULT_SOURCES[key])
|
||||
if buttonchecked:
|
||||
self.tabwidget.setCurrentIndex(thistab)
|
||||
self.button_box = QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box.clicked.connect(self.button_clicked)
|
||||
self.layout.addWidget(self.button_box)
|
||||
set_button_tooltip(self.button_box)
|
||||
selected = super(SourceSelectTabs, self).exec()
|
||||
return selected
|
||||
|
||||
@pyqtSlot(object)
|
||||
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
|
||||
def button_clicked(self, button):
|
||||
"""
|
||||
Checks which button was clicked
|
||||
@ -333,6 +329,9 @@ class SourceSelectTabs(QDialog):
|
||||
return 100
|
||||
|
||||
def delete_sources(self):
|
||||
"""
|
||||
Delete the sources for this projector
|
||||
"""
|
||||
msg = QtWidgets.QMessageBox()
|
||||
msg.setText(translate('OpenLP.SourceSelectForm', 'Delete entries for this projector'))
|
||||
msg.setInformativeText(translate('OpenLP.SourceSelectForm',
|
||||
@ -376,7 +375,7 @@ class SourceSelectTabs(QDialog):
|
||||
self.done(selected)
|
||||
|
||||
|
||||
class SourceSelectSingle(QDialog):
|
||||
class SourceSelectSingle(QtWidgets.QDialog):
|
||||
"""
|
||||
Class for handling selecting the source for the projector to use.
|
||||
Uses single dialog interface.
|
||||
@ -407,12 +406,12 @@ class SourceSelectSingle(QDialog):
|
||||
:param projector: Projector instance to build source list from
|
||||
"""
|
||||
self.projector = projector
|
||||
self.layout = QFormLayout() if self.edit else QVBoxLayout()
|
||||
self.layout = QtWidgets.QFormLayout() if self.edit else QtWidgets.QVBoxLayout()
|
||||
self.layout.setObjectName('source_select_tabs_layout')
|
||||
self.layout.setSpacing(10)
|
||||
self.setLayout(self.layout)
|
||||
self.setMinimumWidth(350)
|
||||
self.button_group = [] if self.edit else QButtonGroup()
|
||||
self.button_group = [] if self.edit else QtWidgets.QButtonGroup()
|
||||
self.source_text = self.projectordb.get_source_list(projector=projector)
|
||||
keys = list(self.source_text.keys())
|
||||
keys.sort()
|
||||
@ -420,7 +419,7 @@ class SourceSelectSingle(QDialog):
|
||||
button_list = []
|
||||
if self.edit:
|
||||
for key in keys:
|
||||
item = QLineEdit()
|
||||
item = QtWidgets.QLineEdit()
|
||||
item.setObjectName('source_key_{key}'.format(key=key))
|
||||
source_item = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id)
|
||||
if source_item is None:
|
||||
@ -430,10 +429,10 @@ class SourceSelectSingle(QDialog):
|
||||
item.setText(source_item.text)
|
||||
self.layout.addRow(PJLINK_DEFAULT_CODES[key], item)
|
||||
self.button_group.append(item)
|
||||
self.button_box = QDialogButtonBox(QtWidgets.QDialogButtonBox.Reset |
|
||||
QtWidgets.QDialogButtonBox.Discard |
|
||||
QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Reset |
|
||||
QtWidgets.QDialogButtonBox.Discard |
|
||||
QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
else:
|
||||
for key in keys:
|
||||
source_text = self.projectordb.get_source_by_code(code=key, projector_id=self.projector.db_item.id)
|
||||
@ -443,8 +442,8 @@ class SourceSelectSingle(QDialog):
|
||||
self.layout.addWidget(button)
|
||||
self.button_group.addButton(button, int(key))
|
||||
button_list.append(key)
|
||||
self.button_box = QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok |
|
||||
QtWidgets.QDialogButtonBox.Cancel)
|
||||
self.button_box.clicked.connect(self.button_clicked)
|
||||
self.layout.addWidget(self.button_box)
|
||||
self.setMinimumHeight(key_count * 25)
|
||||
@ -452,7 +451,7 @@ class SourceSelectSingle(QDialog):
|
||||
selected = super(SourceSelectSingle, self).exec()
|
||||
return selected
|
||||
|
||||
@pyqtSlot(QAbstractButton)
|
||||
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
|
||||
def button_clicked(self, button):
|
||||
"""
|
||||
Checks which button was clicked
|
||||
@ -488,7 +487,7 @@ class SourceSelectSingle(QDialog):
|
||||
self.projectordb.delete_all_objects(ProjectorSource, ProjectorSource.projector_id == self.projector.db_item.id)
|
||||
self.done(100)
|
||||
|
||||
@pyqtSlot()
|
||||
@QtCore.pyqtSlot()
|
||||
def accept_me(self):
|
||||
"""
|
||||
Slot to accept 'OK' button
|
||||
|
@ -22,12 +22,9 @@
|
||||
|
||||
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.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
|
||||
@ -90,7 +87,6 @@ class BiblePlugin(Plugin):
|
||||
action_list.add_action(self.import_bible_item, UiStrings().Import)
|
||||
# Set to invisible until we can export bibles
|
||||
self.export_bible_item.setVisible(False)
|
||||
self.tools_upgrade_item.setVisible(bool(self.manager.old_bible_databases))
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
@ -104,20 +100,6 @@ class BiblePlugin(Plugin):
|
||||
self.import_bible_item.setVisible(False)
|
||||
self.export_bible_item.setVisible(False)
|
||||
|
||||
def app_startup(self):
|
||||
"""
|
||||
Perform tasks on application startup
|
||||
"""
|
||||
super(BiblePlugin, self).app_startup()
|
||||
if self.manager.old_bible_databases:
|
||||
if QtWidgets.QMessageBox.information(
|
||||
self.main_window, translate('OpenLP', 'Information'),
|
||||
translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your '
|
||||
'existing Bibles.\nShould OpenLP upgrade now?'),
|
||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)) == \
|
||||
QtWidgets.QMessageBox.Yes:
|
||||
self.on_tools_upgrade_item_triggered()
|
||||
|
||||
def add_import_menu_item(self, import_menu):
|
||||
"""
|
||||
Add an import menu item
|
||||
@ -139,30 +121,6 @@ class BiblePlugin(Plugin):
|
||||
text=translate('BiblesPlugin', '&Bible'), visible=False)
|
||||
export_menu.addAction(self.export_bible_item)
|
||||
|
||||
def add_tools_menu_item(self, tools_menu):
|
||||
"""
|
||||
Give the bible plugin the opportunity to add items to the **Tools** menu.
|
||||
|
||||
:param tools_menu: The actual **Tools** menu item, so that your actions can use it as their parent.
|
||||
"""
|
||||
log.debug('add tools menu')
|
||||
self.tools_upgrade_item = create_action(
|
||||
tools_menu, 'toolsUpgradeItem',
|
||||
text=translate('BiblesPlugin', '&Upgrade older Bibles'),
|
||||
statustip=translate('BiblesPlugin', 'Upgrade the Bible databases to the latest format.'),
|
||||
visible=False, triggers=self.on_tools_upgrade_item_triggered)
|
||||
tools_menu.addAction(self.tools_upgrade_item)
|
||||
|
||||
def on_tools_upgrade_item_triggered(self):
|
||||
"""
|
||||
Upgrade older bible databases.
|
||||
"""
|
||||
if not hasattr(self, 'upgrade_wizard'):
|
||||
self.upgrade_wizard = BibleUpgradeForm(self.main_window, self.manager, self)
|
||||
# If the import was not cancelled then reload.
|
||||
if self.upgrade_wizard.exec():
|
||||
self.media_item.reload_bibles()
|
||||
|
||||
def on_bible_import_click(self):
|
||||
"""
|
||||
Show the Bible Import wizard
|
||||
|
@ -21,30 +21,12 @@
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
Forms in OpenLP are made up of two classes. One class holds all the graphical elements, like buttons and lists, and the
|
||||
other class holds all the functional code, like slots and loading and saving.
|
||||
|
||||
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
|
||||
modified version of the class that the ``pyuic5`` command produces from Qt5's .ui file. Typical modifications will be
|
||||
converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
|
||||
|
||||
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
|
||||
is instantiated and used. It uses dual inheritance to inherit from (usually) QtWidgets.QDialog and the Ui class
|
||||
mentioned above, like so::
|
||||
|
||||
class BibleImportForm(QtWidgets.QWizard, Ui_BibleImportWizard):
|
||||
|
||||
def __init__(self, parent, manager, bible_plugin):
|
||||
super(BibleImportForm, self).__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
This allows OpenLP to use ``self.object`` for all the GUI elements while keeping them separate from the functionality,
|
||||
so that it is easier to recreate the GUI from the .ui files later if necessary.
|
||||
The :mod:`forms` module contains all the ui functionality for the bibles
|
||||
plugin.
|
||||
"""
|
||||
from .booknameform import BookNameForm
|
||||
from .languageform import LanguageForm
|
||||
from .bibleimportform import BibleImportForm
|
||||
from .bibleupgradeform import BibleUpgradeForm
|
||||
from .editbibleform import EditBibleForm
|
||||
|
||||
__all__ = ['BookNameForm', 'LanguageForm', 'BibleImportForm', 'BibleUpgradeForm', 'EditBibleForm']
|
||||
__all__ = ['BookNameForm', 'LanguageForm', 'BibleImportForm', 'EditBibleForm']
|
||||
|
@ -40,7 +40,7 @@ from openlp.core.ui.lib.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
from openlp.plugins.bibles.lib.manager import BibleFormat
|
||||
from openlp.plugins.bibles.lib.db import clean_filename
|
||||
from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract
|
||||
from openlp.plugins.bibles.lib.importers.http import CWExtract, BGExtract, BSExtract
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,575 +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 bible import functions for OpenLP
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
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.lib.wizard import OpenLPWizard, WizardStrings
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta, OldBibleDB, BiblesResourcesDB
|
||||
from openlp.plugins.bibles.lib.http import BSExtract, BGExtract, CWExtract
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BibleUpgradeForm(OpenLPWizard):
|
||||
"""
|
||||
This is the Bible Upgrade Wizard, which allows easy importing of Bibles into OpenLP from older OpenLP2 database
|
||||
versions.
|
||||
"""
|
||||
log.info('BibleUpgradeForm loaded')
|
||||
|
||||
def __init__(self, parent, manager, bible_plugin):
|
||||
"""
|
||||
Instantiate the wizard, and run any extra setup we need to.
|
||||
|
||||
:param parent: The QWidget-derived parent of the wizard.
|
||||
:param manager: The Bible manager.
|
||||
:param bible_plugin: The Bible plugin.
|
||||
"""
|
||||
self.manager = manager
|
||||
self.media_item = bible_plugin.media_item
|
||||
self.suffix = '.sqlite'
|
||||
self.settings_section = 'bibles'
|
||||
self.path = AppLocation.get_section_data_path(self.settings_section)
|
||||
self.temp_dir = os.path.join(gettempdir(), 'openlp')
|
||||
self.files = self.manager.old_bible_databases
|
||||
self.success = {}
|
||||
self.new_bibles = {}
|
||||
super(BibleUpgradeForm, self).__init__(
|
||||
parent, bible_plugin, 'bibleUpgradeWizard', ':/wizards/wizard_importbible.bmp')
|
||||
|
||||
def setupUi(self, image):
|
||||
"""
|
||||
Set up the UI for the bible wizard.
|
||||
"""
|
||||
super(BibleUpgradeForm, self).setupUi(image)
|
||||
Registry().register_function('openlp_stop_wizard', self.stop_import)
|
||||
|
||||
def stop_import(self):
|
||||
"""
|
||||
Stops the import of the Bible.
|
||||
"""
|
||||
log.debug('Stopping import')
|
||||
self.stop_import_flag = True
|
||||
|
||||
def reject(self):
|
||||
"""
|
||||
Stop the wizard on cancel button, close button or ESC key.
|
||||
"""
|
||||
log.debug('Wizard cancelled by user')
|
||||
self.stop_import_flag = True
|
||||
if not self.currentPage() == self.progress_page:
|
||||
self.done(QtWidgets.QDialog.Rejected)
|
||||
|
||||
def onCurrentIdChanged(self, page_id):
|
||||
"""
|
||||
Perform necessary functions depending on which wizard page is active.
|
||||
"""
|
||||
if self.page(page_id) == self.progress_page:
|
||||
self.pre_wizard()
|
||||
self.perform_wizard()
|
||||
self.post_wizard()
|
||||
elif self.page(page_id) == self.selectPage and not self.files:
|
||||
self.next()
|
||||
|
||||
def on_backup_browse_button_clicked(self):
|
||||
"""
|
||||
Show the file open dialog for the OSIS file.
|
||||
"""
|
||||
filename = QtWidgets.QFileDialog.getExistingDirectory(self, translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Select a Backup Directory'), '')
|
||||
if filename:
|
||||
self.backupDirectoryEdit.setText(filename)
|
||||
|
||||
def on_no_backup_check_box_toggled(self, checked):
|
||||
"""
|
||||
Enable or disable the backup directory widgets.
|
||||
"""
|
||||
self.backupDirectoryEdit.setEnabled(not checked)
|
||||
self.backupBrowseButton.setEnabled(not checked)
|
||||
|
||||
def backup_old_bibles(self, backup_directory):
|
||||
"""
|
||||
Backup old bible databases in a given folder.
|
||||
"""
|
||||
check_directory_exists(backup_directory)
|
||||
success = True
|
||||
for filename in self.files:
|
||||
try:
|
||||
shutil.copy(os.path.join(self.path, filename[0]), backup_directory)
|
||||
except:
|
||||
success = False
|
||||
return success
|
||||
|
||||
def custom_init(self):
|
||||
"""
|
||||
Perform any custom initialisation for bible upgrading.
|
||||
"""
|
||||
self.manager.set_process_dialog(self)
|
||||
self.restart()
|
||||
|
||||
def custom_signals(self):
|
||||
"""
|
||||
Set up the signals used in the bible importer.
|
||||
"""
|
||||
self.backupBrowseButton.clicked.connect(self.on_backup_browse_button_clicked)
|
||||
self.noBackupCheckBox.toggled.connect(self.on_no_backup_check_box_toggled)
|
||||
|
||||
def add_custom_pages(self):
|
||||
"""
|
||||
Add the bible import specific wizard pages.
|
||||
"""
|
||||
# Backup Page
|
||||
self.backup_page = QtWidgets.QWizardPage()
|
||||
self.backup_page.setObjectName('BackupPage')
|
||||
self.backupLayout = QtWidgets.QVBoxLayout(self.backup_page)
|
||||
self.backupLayout.setObjectName('BackupLayout')
|
||||
self.backupInfoLabel = QtWidgets.QLabel(self.backup_page)
|
||||
self.backupInfoLabel.setOpenExternalLinks(True)
|
||||
self.backupInfoLabel.setTextFormat(QtCore.Qt.RichText)
|
||||
self.backupInfoLabel.setWordWrap(True)
|
||||
self.backupInfoLabel.setObjectName('backupInfoLabel')
|
||||
self.backupLayout.addWidget(self.backupInfoLabel)
|
||||
self.selectLabel = QtWidgets.QLabel(self.backup_page)
|
||||
self.selectLabel.setObjectName('select_label')
|
||||
self.backupLayout.addWidget(self.selectLabel)
|
||||
self.formLayout = QtWidgets.QFormLayout()
|
||||
self.formLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.formLayout.setObjectName('FormLayout')
|
||||
self.backupDirectoryLabel = QtWidgets.QLabel(self.backup_page)
|
||||
self.backupDirectoryLabel.setObjectName('backupDirectoryLabel')
|
||||
self.backupDirectoryLayout = QtWidgets.QHBoxLayout()
|
||||
self.backupDirectoryLayout.setObjectName('BackupDirectoryLayout')
|
||||
self.backupDirectoryEdit = QtWidgets.QLineEdit(self.backup_page)
|
||||
self.backupDirectoryEdit.setObjectName('BackupFolderEdit')
|
||||
self.backupDirectoryLayout.addWidget(self.backupDirectoryEdit)
|
||||
self.backupBrowseButton = QtWidgets.QToolButton(self.backup_page)
|
||||
self.backupBrowseButton.setIcon(self.open_icon)
|
||||
self.backupBrowseButton.setObjectName('BackupBrowseButton')
|
||||
self.backupDirectoryLayout.addWidget(self.backupBrowseButton)
|
||||
self.formLayout.addRow(self.backupDirectoryLabel, self.backupDirectoryLayout)
|
||||
self.backupLayout.addLayout(self.formLayout)
|
||||
self.noBackupCheckBox = QtWidgets.QCheckBox(self.backup_page)
|
||||
self.noBackupCheckBox.setObjectName('NoBackupCheckBox')
|
||||
self.backupLayout.addWidget(self.noBackupCheckBox)
|
||||
self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
|
||||
self.backupLayout.addItem(self.spacer)
|
||||
self.addPage(self.backup_page)
|
||||
# Select Page
|
||||
self.selectPage = QtWidgets.QWizardPage()
|
||||
self.selectPage.setObjectName('SelectPage')
|
||||
self.pageLayout = QtWidgets.QVBoxLayout(self.selectPage)
|
||||
self.pageLayout.setObjectName('pageLayout')
|
||||
self.scrollArea = QtWidgets.QScrollArea(self.selectPage)
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollArea.setObjectName('scrollArea')
|
||||
self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.scrollAreaContents = QtWidgets.QWidget(self.scrollArea)
|
||||
self.scrollAreaContents.setObjectName('scrollAreaContents')
|
||||
self.formLayout = QtWidgets.QVBoxLayout(self.scrollAreaContents)
|
||||
self.formLayout.setSpacing(2)
|
||||
self.formLayout.setObjectName('formLayout')
|
||||
self.addScrollArea()
|
||||
self.pageLayout.addWidget(self.scrollArea)
|
||||
self.addPage(self.selectPage)
|
||||
|
||||
def addScrollArea(self):
|
||||
"""
|
||||
Add the content to the scrollArea.
|
||||
"""
|
||||
self.checkBox = {}
|
||||
for number, filename in enumerate(self.files):
|
||||
bible = OldBibleDB(self.media_item, path=self.path, file=filename[0])
|
||||
self.checkBox[number] = QtWidgets.QCheckBox(self.scrollAreaContents)
|
||||
self.checkBox[number].setObjectName('checkBox[{count:d}]'.format(count=number))
|
||||
self.checkBox[number].setText(bible.get_name())
|
||||
self.checkBox[number].setCheckState(QtCore.Qt.Checked)
|
||||
self.formLayout.addWidget(self.checkBox[number])
|
||||
self.spacer_item = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.formLayout.addItem(self.spacer_item)
|
||||
self.scrollArea.setWidget(self.scrollAreaContents)
|
||||
|
||||
def clearScrollArea(self):
|
||||
"""
|
||||
Remove the content from the scrollArea.
|
||||
"""
|
||||
for number, filename in enumerate(self.files):
|
||||
self.formLayout.removeWidget(self.checkBox[number])
|
||||
self.checkBox[number].setParent(None)
|
||||
self.formLayout.removeItem(self.spacer_item)
|
||||
|
||||
def retranslateUi(self):
|
||||
"""
|
||||
Allow for localisation of the bible import wizard.
|
||||
"""
|
||||
self.setWindowTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Bible Upgrade Wizard'))
|
||||
self.title_label.setText(WizardStrings.HeaderStyle % translate('OpenLP.Ui',
|
||||
'Welcome to the Bible Upgrade Wizard'))
|
||||
self.information_label.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'This wizard will help you to upgrade your existing Bibles from a prior version of OpenLP 2. '
|
||||
'Click the next button below to start the upgrade process.'))
|
||||
self.backup_page.setTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Select Backup Directory'))
|
||||
self.backup_page.setSubTitle(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'Please select a backup directory for your Bibles'))
|
||||
self.backupInfoLabel.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Previous releases of OpenLP 2.0 are unable to use upgraded Bibles.'
|
||||
' This will create a backup of your current Bibles so that you can '
|
||||
'simply copy the files back to your OpenLP data directory if you '
|
||||
'need to revert to a previous release of OpenLP. Instructions on '
|
||||
'how to restore the files can be found in our <a href="'
|
||||
'http://wiki.openlp.org/faq">Frequently Asked Questions</a>.'))
|
||||
self.selectLabel.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'Please select a backup location for your Bibles.'))
|
||||
self.backupDirectoryLabel.setText(translate('BiblesPlugin.UpgradeWizardForm', 'Backup Directory:'))
|
||||
self.noBackupCheckBox.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'There is no need to backup my Bibles'))
|
||||
self.selectPage.setTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Select Bibles'))
|
||||
self.selectPage.setSubTitle(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'Please select the Bibles to upgrade'))
|
||||
self.progress_page.setTitle(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrading'))
|
||||
self.progress_page.setSubTitle(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'Please wait while your Bibles are upgraded.'))
|
||||
self.progress_label.setText(WizardStrings.Ready)
|
||||
self.progress_bar.setFormat('%p%')
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validate the current page before moving on to the next page.
|
||||
"""
|
||||
if self.currentPage() == self.welcome_page:
|
||||
return True
|
||||
elif self.currentPage() == self.backup_page:
|
||||
if not self.noBackupCheckBox.checkState() == QtCore.Qt.Checked:
|
||||
backup_path = self.backupDirectoryEdit.text()
|
||||
if not backup_path:
|
||||
critical_error_message_box(
|
||||
UiStrings().EmptyField,
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'You need to specify a backup directory for '
|
||||
'your Bibles.'))
|
||||
self.backupDirectoryEdit.setFocus()
|
||||
return False
|
||||
else:
|
||||
if not self.backup_old_bibles(backup_path):
|
||||
critical_error_message_box(
|
||||
UiStrings().Error,
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'The backup was not successful.\nTo backup '
|
||||
'your Bibles you need permission to write to the given directory.'))
|
||||
return False
|
||||
return True
|
||||
elif self.currentPage() == self.selectPage:
|
||||
check_directory_exists(self.temp_dir)
|
||||
for number, filename in enumerate(self.files):
|
||||
if not self.checkBox[number].checkState() == QtCore.Qt.Checked:
|
||||
continue
|
||||
# Move bibles to temp dir.
|
||||
if not os.path.exists(os.path.join(self.temp_dir, filename[0])):
|
||||
shutil.move(os.path.join(self.path, filename[0]), self.temp_dir)
|
||||
else:
|
||||
delete_file(os.path.join(self.path, filename[0]))
|
||||
return True
|
||||
if self.currentPage() == self.progress_page:
|
||||
return True
|
||||
|
||||
def set_defaults(self):
|
||||
"""
|
||||
Set default values for the wizard pages.
|
||||
"""
|
||||
log.debug('BibleUpgrade set_defaults')
|
||||
settings = Settings()
|
||||
settings.beginGroup(self.plugin.settings_section)
|
||||
self.stop_import_flag = False
|
||||
self.success.clear()
|
||||
self.new_bibles.clear()
|
||||
self.clearScrollArea()
|
||||
self.files = self.manager.old_bible_databases
|
||||
self.addScrollArea()
|
||||
self.retranslateUi()
|
||||
for number, filename in enumerate(self.files):
|
||||
self.checkBox[number].setCheckState(QtCore.Qt.Checked)
|
||||
self.progress_bar.show()
|
||||
self.restart()
|
||||
self.finish_button.setVisible(False)
|
||||
self.cancel_button.setVisible(True)
|
||||
settings.endGroup()
|
||||
|
||||
def pre_wizard(self):
|
||||
"""
|
||||
Prepare the UI for the upgrade.
|
||||
"""
|
||||
super(BibleUpgradeForm, self).pre_wizard()
|
||||
self.progress_label.setText(translate('BiblesPlugin.UpgradeWizardForm', 'Starting upgrade...'))
|
||||
self.application.process_events()
|
||||
|
||||
def perform_wizard(self):
|
||||
"""
|
||||
Perform the actual upgrade.
|
||||
"""
|
||||
self.includeWebBible = False
|
||||
proxy_server = None
|
||||
if not self.files:
|
||||
self.progress_label.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'There are no Bibles that need to be upgraded.'))
|
||||
self.progress_bar.hide()
|
||||
return
|
||||
max_bibles = 0
|
||||
for number, file in enumerate(self.files):
|
||||
if self.checkBox[number].checkState() == QtCore.Qt.Checked:
|
||||
max_bibles += 1
|
||||
old_bible = None
|
||||
for number, filename in enumerate(self.files):
|
||||
# Close the previous bible's connection.
|
||||
if old_bible is not None:
|
||||
old_bible.close_connection()
|
||||
# Set to None to make obvious that we have already closed the
|
||||
# database.
|
||||
old_bible = None
|
||||
if self.stop_import_flag:
|
||||
self.success[number] = False
|
||||
break
|
||||
if not self.checkBox[number].checkState() == QtCore.Qt.Checked:
|
||||
self.success[number] = False
|
||||
continue
|
||||
self.progress_bar.reset()
|
||||
old_bible = OldBibleDB(self.media_item, path=self.temp_dir, file=filename[0])
|
||||
name = filename[1]
|
||||
self.progress_label.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Upgrading ...').format(count=number + 1,
|
||||
total=max_bibles,
|
||||
name=name))
|
||||
self.new_bibles[number] = BibleDB(self.media_item, path=self.path, name=name, file=filename[0])
|
||||
self.new_bibles[number].register(self.plugin.upgrade_wizard)
|
||||
metadata = old_bible.get_metadata()
|
||||
web_bible = False
|
||||
meta_data = {}
|
||||
for meta in metadata:
|
||||
# Upgrade the names of the metadata keys
|
||||
if meta['key'] == 'Version':
|
||||
meta['key'] = 'name'
|
||||
if meta['key'] == 'Bookname language':
|
||||
meta['key'] = 'book_name_language'
|
||||
meta['key'] = meta['key'].lower().replace(' ', '_')
|
||||
# Copy the metadata
|
||||
meta_data[meta['key']] = meta['value']
|
||||
if meta['key'] != 'name' and meta['key'] != 'dbversion':
|
||||
self.new_bibles[number].save_meta(meta['key'], meta['value'])
|
||||
if meta['key'] == 'download_source':
|
||||
web_bible = True
|
||||
self.includeWebBible = True
|
||||
proxy_server = meta.get('proxy_server')
|
||||
if web_bible:
|
||||
if meta_data['download_source'].lower() == 'crosswalk':
|
||||
handler = CWExtract(proxy_server)
|
||||
elif meta_data['download_source'].lower() == 'biblegateway':
|
||||
handler = BGExtract(proxy_server)
|
||||
elif meta_data['download_source'].lower() == 'bibleserver':
|
||||
handler = BSExtract(proxy_server)
|
||||
books = handler.get_books_from_http(meta_data['download_name'])
|
||||
if not books:
|
||||
log.error('Upgrading books from {uri} - '
|
||||
'download name: "{name}" failed'.format(uri=meta_data['download_source'],
|
||||
name=meta_data['download_name']))
|
||||
self.new_bibles[number].session.close()
|
||||
del self.new_bibles[number]
|
||||
critical_error_message_box(
|
||||
translate('BiblesPlugin.UpgradeWizardForm', 'Download Error'),
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'To upgrade your Web Bibles an Internet connection is required.'))
|
||||
text = translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Failed').format(count=number + 1, total=max_bibles, name=name)
|
||||
self.increment_progress_bar(text, self.progress_bar.maximum() - self.progress_bar.value())
|
||||
self.success[number] = False
|
||||
continue
|
||||
bible = BiblesResourcesDB.get_webbible(
|
||||
meta_data['download_name'],
|
||||
meta_data['download_source'].lower())
|
||||
if bible and bible['language_id']:
|
||||
language_id = bible['language_id']
|
||||
self.new_bibles[number].save_meta('language_id', language_id)
|
||||
else:
|
||||
language_id = self.new_bibles[number].get_language(name)
|
||||
if not language_id:
|
||||
log.warning('Upgrading from "{name}" failed'.format(name=filename[0]))
|
||||
self.new_bibles[number].session.close()
|
||||
del self.new_bibles[number]
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Failed').format(count=number + 1, total=max_bibles, name=name),
|
||||
self.progress_bar.maximum() - self.progress_bar.value())
|
||||
self.success[number] = False
|
||||
continue
|
||||
self.progress_bar.setMaximum(len(books))
|
||||
for book in books:
|
||||
if self.stop_import_flag:
|
||||
self.success[number] = False
|
||||
break
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Upgrading {book} ...').format(count=number + 1, total=max_bibles,
|
||||
name=name, book=book))
|
||||
book_ref_id = self.new_bibles[number].\
|
||||
get_book_ref_id_by_name(book, len(books), language_id)
|
||||
if not book_ref_id:
|
||||
log.warning('Upgrading books from {source} - download name: "{name}" '
|
||||
'aborted by user'.format(source=meta_data['download_source'],
|
||||
name=meta_data['download_name']))
|
||||
self.new_bibles[number].session.close()
|
||||
del self.new_bibles[number]
|
||||
self.success[number] = False
|
||||
break
|
||||
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
||||
db_book = self.new_bibles[number].create_book(book, book_ref_id, book_details['testament_id'])
|
||||
# Try to import already downloaded verses.
|
||||
oldbook = old_bible.get_book(book)
|
||||
if oldbook:
|
||||
verses = old_bible.get_verses(oldbook['id'])
|
||||
if not verses:
|
||||
log.warning('No verses found to import for book "{book}"'.format(book=book))
|
||||
continue
|
||||
for verse in verses:
|
||||
if self.stop_import_flag:
|
||||
self.success[number] = False
|
||||
break
|
||||
self.new_bibles[number].create_verse(db_book.id, int(verse['chapter']),
|
||||
int(verse['verse']), str(verse['text']))
|
||||
self.application.process_events()
|
||||
self.new_bibles[number].session.commit()
|
||||
else:
|
||||
language_id = self.new_bibles[number].get_object(BibleMeta, 'language_id')
|
||||
if not language_id:
|
||||
language_id = self.new_bibles[number].get_language(name)
|
||||
if not language_id:
|
||||
log.warning('Upgrading books from "{name}" failed'.format(name=name))
|
||||
self.new_bibles[number].session.close()
|
||||
del self.new_bibles[number]
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Failed').format(count=number + 1, total=max_bibles, name=name),
|
||||
self.progress_bar.maximum() - self.progress_bar.value())
|
||||
self.success[number] = False
|
||||
continue
|
||||
books = old_bible.get_books()
|
||||
self.progress_bar.setMaximum(len(books))
|
||||
for book in books:
|
||||
if self.stop_import_flag:
|
||||
self.success[number] = False
|
||||
break
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Upgrading {book} ...').format(count=number + 1, total=max_bibles,
|
||||
name=name, book=book['name']))
|
||||
book_ref_id = self.new_bibles[number].get_book_ref_id_by_name(book['name'], len(books), language_id)
|
||||
if not book_ref_id:
|
||||
log.warning('Upgrading books from {name} " failed - aborted by user'.format(name=name))
|
||||
self.new_bibles[number].session.close()
|
||||
del self.new_bibles[number]
|
||||
self.success[number] = False
|
||||
break
|
||||
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
||||
db_book = self.new_bibles[number].create_book(book['name'], book_ref_id,
|
||||
book_details['testament_id'])
|
||||
verses = old_bible.get_verses(book['id'])
|
||||
if not verses:
|
||||
log.warning('No verses found to import for book "{book}"'.format(book=book['name']))
|
||||
self.new_bibles[number].delete_book(db_book)
|
||||
continue
|
||||
for verse in verses:
|
||||
if self.stop_import_flag:
|
||||
self.success[number] = False
|
||||
break
|
||||
self.new_bibles[number].create_verse(db_book.id, int(verse['chapter']), int(verse['verse']),
|
||||
str(verse['text']))
|
||||
self.application.process_events()
|
||||
self.new_bibles[number].session.commit()
|
||||
if not self.success.get(number, True):
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Failed').format(count=number + 1, total=max_bibles, name=name),
|
||||
self.progress_bar.maximum() - self.progress_bar.value())
|
||||
else:
|
||||
self.success[number] = True
|
||||
self.new_bibles[number].save_meta('name', name)
|
||||
self.increment_progress_bar(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible {count} of {total}: "{name}"\n'
|
||||
'Complete').format(count=number + 1, total=max_bibles, name=name))
|
||||
if number in self.new_bibles:
|
||||
self.new_bibles[number].session.close()
|
||||
# Close the last bible's connection if possible.
|
||||
if old_bible is not None:
|
||||
old_bible.close_connection()
|
||||
|
||||
def post_wizard(self):
|
||||
"""
|
||||
Clean up the UI after the import has finished.
|
||||
"""
|
||||
successful_import = 0
|
||||
failed_import = 0
|
||||
for number, filename in enumerate(self.files):
|
||||
if self.success.get(number):
|
||||
successful_import += 1
|
||||
elif self.checkBox[number].checkState() == QtCore.Qt.Checked:
|
||||
failed_import += 1
|
||||
# Delete upgraded (but not complete, corrupted, ...) bible.
|
||||
delete_file(os.path.join(self.path, filename[0]))
|
||||
# Copy not upgraded bible back.
|
||||
shutil.move(os.path.join(self.temp_dir, filename[0]), self.path)
|
||||
if failed_import > 0:
|
||||
failed_import_text = translate('BiblesPlugin.UpgradeWizardForm',
|
||||
', {name} failed').format(name=failed_import)
|
||||
else:
|
||||
failed_import_text = ''
|
||||
if successful_import > 0:
|
||||
if self.includeWebBible:
|
||||
self.progress_label.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible(s): {count:d} successful{failed}\nPlease note that verses '
|
||||
'from Web Bibles will be downloaded on demand and so an Internet connection is required.'
|
||||
).format(count=successful_import, failed=failed_import_text))
|
||||
else:
|
||||
self.progress_label.setText(
|
||||
translate('BiblesPlugin.UpgradeWizardForm',
|
||||
'Upgrading Bible(s): {count:d} successful{failed}').format(count=successful_import,
|
||||
failed=failed_import_text))
|
||||
else:
|
||||
self.progress_label.setText(translate('BiblesPlugin.UpgradeWizardForm', 'Upgrade failed.'))
|
||||
# Remove temp directory.
|
||||
shutil.rmtree(self.temp_dir, True)
|
||||
super(BibleUpgradeForm, self).post_wizard()
|
113
openlp/plugins/bibles/lib/bibleimport.py
Normal file
113
openlp/plugins/bibles/lib/bibleimport.py
Normal file
@ -0,0 +1,113 @@
|
||||
# -*- 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 #
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
|
||||
from lxml import etree, objectify
|
||||
|
||||
from openlp.core.common import OpenLPMixin, languages
|
||||
from openlp.core.lib import ValidationError
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BibleImport(OpenLPMixin, BibleDB):
|
||||
"""
|
||||
Helper class to import bibles from a third party source into OpenLP
|
||||
"""
|
||||
# TODO: Test
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filename = kwargs['filename'] if 'filename' in kwargs else None
|
||||
|
||||
def get_language_id(self, file_language=None, bible_name=None):
|
||||
"""
|
||||
Get the language_id for the language of the bible. Fallback to user input if we cannot do this pragmatically.
|
||||
|
||||
:param file_language: Language of the bible. Possibly retrieved from the file being imported. Str
|
||||
:param bible_name: Name of the bible to display on the get_language dialog. Str
|
||||
:return: The id of a language Int or None
|
||||
"""
|
||||
language_id = None
|
||||
if file_language:
|
||||
language = languages.get_language(file_language)
|
||||
if language and language.id:
|
||||
language_id = language.id
|
||||
if not language_id:
|
||||
# The language couldn't be detected, ask the user
|
||||
language_id = self.get_language(bible_name)
|
||||
if not language_id:
|
||||
# User cancelled get_language dialog
|
||||
log.error('Language detection failed when importing from "{name}". User aborted language selection.'
|
||||
.format(name=bible_name))
|
||||
return None
|
||||
self.save_meta('language_id', language_id)
|
||||
return language_id
|
||||
|
||||
def find_and_create_book(self, name, no_of_books, language_id, guess_id=None):
|
||||
"""
|
||||
Find the OpenLP book id and then create the book in this bible db
|
||||
|
||||
:param name: Name of the book. If None, then fall back to the guess_id Str
|
||||
:param no_of_books: The total number of books contained in this bible Int
|
||||
:param language_id: The OpenLP id of the language of this bible Int
|
||||
:param guess_id: The guessed id of the book, used if name is None Int
|
||||
:return:
|
||||
"""
|
||||
if name:
|
||||
book_ref_id = self.get_book_ref_id_by_name(name, no_of_books, language_id)
|
||||
else:
|
||||
log.debug('No book name supplied. Falling back to guess_id')
|
||||
book_ref_id = guess_id
|
||||
if not book_ref_id:
|
||||
raise ValidationError(msg='Could not resolve book_ref_id in "{}"'.format(self.filename))
|
||||
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
||||
if book_details is None:
|
||||
raise ValidationError(msg='book_ref_id: {book_ref} Could not be found in the BibleResourcesDB while '
|
||||
'importing {file}'.format(book_ref=book_ref_id, file=self.filename))
|
||||
return self.create_book(name, book_ref_id, book_details['testament_id'])
|
||||
|
||||
@staticmethod
|
||||
def parse_xml(filename, use_objectify=False, elements=None, tags=None):
|
||||
"""
|
||||
Parse and clean the supplied file by removing any elements or tags we don't use.
|
||||
:param filename: The filename of the xml file to parse. Str
|
||||
:param use_objectify: Use the objectify parser rather than the etree parser. (Bool)
|
||||
:param elements: A tuple of element names (Str) to remove along with their content.
|
||||
:param tags: A tuple of element names (Str) to remove, preserving their content.
|
||||
:return: The root element of the xml document
|
||||
"""
|
||||
with open(filename, 'rb') as import_file:
|
||||
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
||||
# detection, and the two mechanisms together interfere with each other.
|
||||
if not use_objectify:
|
||||
tree = etree.parse(import_file, parser=etree.XMLParser(recover=True))
|
||||
else:
|
||||
tree = objectify.parse(import_file, parser=objectify.makeparser(recover=True))
|
||||
if elements:
|
||||
# Strip tags we don't use - remove content
|
||||
etree.strip_elements(tree, elements, with_tail=False)
|
||||
if tags:
|
||||
# Strip tags we don't use - keep content
|
||||
etree.strip_tags(tree, tags)
|
||||
return tree.getroot()
|
@ -1,173 +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:`cvsbible` modules provides a facility to import bibles from a set of CSV files.
|
||||
|
||||
The module expects two mandatory files containing the books and the verses.
|
||||
|
||||
The format of the books file is:
|
||||
|
||||
<book_id>,<testament_id>,<book_name>,<book_abbreviation>
|
||||
|
||||
For example
|
||||
|
||||
1,1,Genesis,Gen
|
||||
2,1,Exodus,Exod
|
||||
...
|
||||
40,2,Matthew,Matt
|
||||
|
||||
There are two acceptable formats of the verses file. They are:
|
||||
|
||||
<book_id>,<chapter_number>,<verse_number>,<verse_text>
|
||||
or
|
||||
<book_name>,<chapter_number>,<verse_number>,<verse_text>
|
||||
|
||||
For example:
|
||||
|
||||
1,1,1,"In the beginning God created the heaven and the earth."
|
||||
or
|
||||
"Genesis",1,2,"And the earth was without form, and void; and...."
|
||||
|
||||
All CSV files are expected to use a comma (',') as the delimiter and double quotes ('"') as the quote symbol.
|
||||
"""
|
||||
import logging
|
||||
import chardet
|
||||
import csv
|
||||
|
||||
from openlp.core.common import translate
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CSVBible(BibleDB):
|
||||
"""
|
||||
This class provides a specialisation for importing of CSV Bibles.
|
||||
"""
|
||||
log.info('CSVBible loaded')
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
"""
|
||||
Loads a Bible from a set of CSV files. This class assumes the files contain all the information and a clean
|
||||
bible is being loaded.
|
||||
"""
|
||||
log.info(self.__class__.__name__)
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
self.books_file = kwargs['booksfile']
|
||||
self.verses_file = kwargs['versefile']
|
||||
|
||||
def do_import(self, bible_name=None):
|
||||
"""
|
||||
Import the bible books and verses.
|
||||
"""
|
||||
self.wizard.progress_bar.setValue(0)
|
||||
self.wizard.progress_bar.setMinimum(0)
|
||||
self.wizard.progress_bar.setMaximum(66)
|
||||
success = True
|
||||
language_id = self.get_language(bible_name)
|
||||
if not language_id:
|
||||
log.error('Importing books from "{name}" failed'.format(name=self.filename))
|
||||
return False
|
||||
books_file = None
|
||||
book_list = {}
|
||||
# Populate the Tables
|
||||
try:
|
||||
details = get_file_encoding(self.books_file)
|
||||
books_file = open(self.books_file, 'r', encoding=details['encoding'])
|
||||
books_reader = csv.reader(books_file, delimiter=',', quotechar='"')
|
||||
for line in books_reader:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.CSVBible',
|
||||
'Importing books... {text}').format(text=line[2]))
|
||||
book_ref_id = self.get_book_ref_id_by_name(line[2], 67, language_id)
|
||||
if not book_ref_id:
|
||||
log.error('Importing books from "{name}" failed'.format(name=self.books_file))
|
||||
return False
|
||||
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
||||
self.create_book(line[2], book_ref_id, book_details['testament_id'])
|
||||
book_list.update({int(line[0]): line[2]})
|
||||
self.application.process_events()
|
||||
except (IOError, IndexError):
|
||||
log.exception('Loading books from file failed')
|
||||
success = False
|
||||
finally:
|
||||
if books_file:
|
||||
books_file.close()
|
||||
if self.stop_import_flag or not success:
|
||||
return False
|
||||
self.wizard.progress_bar.setValue(0)
|
||||
self.wizard.progress_bar.setMaximum(67)
|
||||
verse_file = None
|
||||
try:
|
||||
book_ptr = None
|
||||
details = get_file_encoding(self.verses_file)
|
||||
verse_file = open(self.verses_file, 'r', encoding=details['encoding'])
|
||||
verse_reader = csv.reader(verse_file, delimiter=',', quotechar='"')
|
||||
for line in verse_reader:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
try:
|
||||
line_book = book_list[int(line[0])]
|
||||
except ValueError:
|
||||
line_book = line[0]
|
||||
if book_ptr != line_book:
|
||||
book = self.get_book(line_book)
|
||||
book_ptr = book.name
|
||||
# TODO: Check out this conversion in translations
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.CSVBible',
|
||||
'Importing verses from {name}...'.format(name=book.name),
|
||||
'Importing verses from <book name>...'))
|
||||
self.session.commit()
|
||||
verse_text = line[3]
|
||||
self.create_verse(book.id, line[1], line[2], verse_text)
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.CSVBible', 'Importing verses... done.'))
|
||||
self.application.process_events()
|
||||
self.session.commit()
|
||||
except IOError:
|
||||
log.exception('Loading verses from file failed')
|
||||
success = False
|
||||
finally:
|
||||
if verse_file:
|
||||
verse_file.close()
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
else:
|
||||
return success
|
||||
|
||||
|
||||
def get_file_encoding(filename):
|
||||
"""
|
||||
Utility function to get the file encoding.
|
||||
"""
|
||||
detect_file = None
|
||||
try:
|
||||
detect_file = open(filename, 'rb')
|
||||
details = chardet.detect(detect_file.read(1024))
|
||||
except IOError:
|
||||
log.exception('Error detecting file encoding')
|
||||
finally:
|
||||
if detect_file:
|
||||
detect_file.close()
|
||||
return details
|
@ -477,7 +477,7 @@ class BibleDB(Manager, RegistryProperties):
|
||||
combo_box = language_form.language_combo_box
|
||||
language_id = combo_box.itemData(combo_box.currentIndex())
|
||||
if not language_id:
|
||||
return False
|
||||
return None
|
||||
self.save_meta('language_id', language_id)
|
||||
return language_id
|
||||
|
||||
@ -864,138 +864,3 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
|
||||
return AlternativeBookNamesDB.run_sql(
|
||||
'INSERT INTO alternative_book_names(book_reference_id, language_id, name) '
|
||||
'VALUES (?, ?, ?)', (book_reference_id, language_id, name), True)
|
||||
|
||||
|
||||
class OldBibleDB(QtCore.QObject, Manager):
|
||||
"""
|
||||
This class connects to the old bible databases to reimport them to the new
|
||||
database scheme.
|
||||
"""
|
||||
cursor = None
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
"""
|
||||
The constructor loads up the database and creates and initialises the tables if the database doesn't exist.
|
||||
|
||||
**Required keyword arguments:**
|
||||
|
||||
``path``
|
||||
The path to the bible database file.
|
||||
|
||||
``name``
|
||||
The name of the database. This is also used as the file name for SQLite databases.
|
||||
"""
|
||||
log.info('OldBibleDB loaded')
|
||||
QtCore.QObject.__init__(self)
|
||||
if 'path' not in kwargs:
|
||||
raise KeyError('Missing keyword argument "path".')
|
||||
if 'file' not in kwargs:
|
||||
raise KeyError('Missing keyword argument "file".')
|
||||
if 'path' in kwargs:
|
||||
self.path = kwargs['path']
|
||||
if 'file' in kwargs:
|
||||
self.file = kwargs['file']
|
||||
|
||||
def get_cursor(self):
|
||||
"""
|
||||
Return the cursor object. Instantiate one if it doesn't exist yet.
|
||||
"""
|
||||
if self.cursor is None:
|
||||
file_path = os.path.join(self.path, self.file)
|
||||
self.connection = sqlite3.connect(file_path)
|
||||
self.cursor = self.connection.cursor()
|
||||
return self.cursor
|
||||
|
||||
def run_sql(self, query, parameters=()):
|
||||
"""
|
||||
Run an SQL query on the database, returning the results.
|
||||
|
||||
:param query: The actual SQL query to run.
|
||||
:param parameters: Any variable parameters to add to the query.
|
||||
"""
|
||||
cursor = self.get_cursor()
|
||||
cursor.execute(query, parameters)
|
||||
return cursor.fetchall()
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Returns the version name of the Bible.
|
||||
"""
|
||||
self.name = None
|
||||
version_name = self.run_sql('SELECT value FROM metadata WHERE key = "name"')
|
||||
if version_name:
|
||||
self.name = version_name[0][0]
|
||||
else:
|
||||
# Fallback to old way of naming
|
||||
version_name = self.run_sql('SELECT value FROM metadata WHERE key = "Version"')
|
||||
if version_name:
|
||||
self.name = version_name[0][0]
|
||||
return self.name
|
||||
|
||||
def get_metadata(self):
|
||||
"""
|
||||
Returns the metadata of the Bible.
|
||||
"""
|
||||
metadata = self.run_sql('SELECT key, value FROM metadata ORDER BY rowid')
|
||||
if metadata:
|
||||
return [{
|
||||
'key': str(meta[0]),
|
||||
'value': str(meta[1])
|
||||
} for meta in metadata]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_book(self, name):
|
||||
"""
|
||||
Return a book by name or abbreviation.
|
||||
|
||||
``name``
|
||||
The name or abbreviation of the book.
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
name = str(name)
|
||||
books = self.run_sql(
|
||||
'SELECT id, testament_id, name, abbreviation FROM book WHERE LOWER(name) = ? OR '
|
||||
'LOWER(abbreviation) = ?', (name.lower(), name.lower()))
|
||||
if books:
|
||||
return {
|
||||
'id': books[0][0],
|
||||
'testament_id': books[0][1],
|
||||
'name': str(books[0][2]),
|
||||
'abbreviation': str(books[0][3])
|
||||
}
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_books(self):
|
||||
"""
|
||||
Returns the books of the Bible.
|
||||
"""
|
||||
books = self.run_sql('SELECT name, id FROM book ORDER BY id')
|
||||
if books:
|
||||
return [{
|
||||
'name': str(book[0]),
|
||||
'id':int(book[1])
|
||||
} for book in books]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_verses(self, book_id):
|
||||
"""
|
||||
Returns the verses of the Bible.
|
||||
"""
|
||||
verses = self.run_sql(
|
||||
'SELECT book_id, chapter, verse, text FROM verse WHERE book_id = ? ORDER BY id', (book_id, ))
|
||||
if verses:
|
||||
return [{
|
||||
'book_id': int(verse[0]),
|
||||
'chapter': int(verse[1]),
|
||||
'verse': int(verse[2]),
|
||||
'text': str(verse[3])
|
||||
} for verse in verses]
|
||||
else:
|
||||
return None
|
||||
|
||||
def close_connection(self):
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
|
186
openlp/plugins/bibles/lib/importers/csvbible.py
Normal file
186
openlp/plugins/bibles/lib/importers/csvbible.py
Normal file
@ -0,0 +1,186 @@
|
||||
# -*- 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:`cvsbible` modules provides a facility to import bibles from a set of CSV files.
|
||||
|
||||
The module expects two mandatory files containing the books and the verses.
|
||||
|
||||
The format of the books file is:
|
||||
|
||||
<book_id>,<testament_id>,<book_name>,<book_abbreviation>
|
||||
|
||||
For example
|
||||
|
||||
1,1,Genesis,Gen
|
||||
2,1,Exodus,Exod
|
||||
...
|
||||
40,2,Matthew,Matt
|
||||
|
||||
There are two acceptable formats of the verses file. They are:
|
||||
|
||||
<book_id>,<chapter_number>,<verse_number>,<verse_text>
|
||||
or
|
||||
<book_name>,<chapter_number>,<verse_number>,<verse_text>
|
||||
|
||||
For example:
|
||||
|
||||
1,1,1,"In the beginning God created the heaven and the earth."
|
||||
or
|
||||
"Genesis",1,2,"And the earth was without form, and void; and...."
|
||||
|
||||
All CSV files are expected to use a comma (',') as the delimiter and double quotes ('"') as the quote symbol.
|
||||
"""
|
||||
import csv
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from openlp.core.common import get_file_encoding, translate
|
||||
from openlp.core.lib.exceptions import ValidationError
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
Book = namedtuple('Book', 'id, testament_id, name, abbreviation')
|
||||
Verse = namedtuple('Verse', 'book_id_name, chapter_number, number, text')
|
||||
|
||||
|
||||
class CSVBible(BibleImport):
|
||||
"""
|
||||
This class provides a specialisation for importing of CSV Bibles.
|
||||
"""
|
||||
log.info('CSVBible loaded')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Loads a Bible from a set of CSV files. This class assumes the files contain all the information and a clean
|
||||
bible is being loaded.
|
||||
"""
|
||||
log.info(self.__class__.__name__)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.books_file = kwargs['booksfile']
|
||||
self.verses_file = kwargs['versefile']
|
||||
|
||||
@staticmethod
|
||||
def get_book_name(name, books):
|
||||
"""
|
||||
Normalize a book name or id.
|
||||
|
||||
:param name: The name, or id of a book. Str
|
||||
:param books: A dict of books parsed from the books file.
|
||||
:return: The normalized name. Str
|
||||
"""
|
||||
try:
|
||||
book_name = books[int(name)]
|
||||
except ValueError:
|
||||
book_name = name
|
||||
return book_name
|
||||
|
||||
@staticmethod
|
||||
def parse_csv_file(filename, results_tuple):
|
||||
"""
|
||||
Parse the supplied CSV file.
|
||||
|
||||
:param filename: The name of the file to parse. Str
|
||||
:param results_tuple: The namedtuple to use to store the results. namedtuple
|
||||
:return: An iterable yielding namedtuples of type results_tuple
|
||||
"""
|
||||
try:
|
||||
encoding = get_file_encoding(filename)['encoding']
|
||||
with open(filename, 'r', encoding=encoding, newline='') as csv_file:
|
||||
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
|
||||
return [results_tuple(*line) for line in csv_reader]
|
||||
except (OSError, csv.Error):
|
||||
raise ValidationError(msg='Parsing "{file}" failed'.format(file=filename))
|
||||
|
||||
def process_books(self, books):
|
||||
"""
|
||||
Process the books parsed from the books file.
|
||||
|
||||
:param books: An a list Book namedtuples
|
||||
:return: A dict of books or None
|
||||
"""
|
||||
book_list = {}
|
||||
number_of_books = len(books)
|
||||
for book in books:
|
||||
if self.stop_import_flag:
|
||||
return None
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.CSVBible', 'Importing books... {book}').format(book=book.name))
|
||||
self.find_and_create_book(book.name, number_of_books, self.language_id)
|
||||
book_list.update({int(book.id): book.name})
|
||||
self.application.process_events()
|
||||
return book_list
|
||||
|
||||
def process_verses(self, verses, books):
|
||||
"""
|
||||
Process the verses parsed from the verses file.
|
||||
|
||||
:param verses: A list of Verse namedtuples
|
||||
:param books: A dict of books
|
||||
:return: None
|
||||
"""
|
||||
book_ptr = None
|
||||
for verse in verses:
|
||||
if self.stop_import_flag:
|
||||
return None
|
||||
verse_book = self.get_book_name(verse.book_id_name, books)
|
||||
if book_ptr != verse_book:
|
||||
book = self.get_book(verse_book)
|
||||
book_ptr = book.name
|
||||
self.wizard.increment_progress_bar(
|
||||
translate('BiblesPlugin.CSVBible', 'Importing verses from {book}...',
|
||||
'Importing verses from <book name>...').format(book=book.name))
|
||||
self.session.commit()
|
||||
self.create_verse(book.id, verse.chapter_number, verse.number, verse.text)
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.CSVBible', 'Importing verses... done.'))
|
||||
self.application.process_events()
|
||||
self.session.commit()
|
||||
|
||||
def do_import(self, bible_name=None):
|
||||
"""
|
||||
Import a bible from the CSV files.
|
||||
|
||||
:param bible_name: Optional name of the bible being imported. Str or None
|
||||
:return: True if the import was successful, False if it failed or was cancelled
|
||||
"""
|
||||
try:
|
||||
self.language_id = self.get_language(bible_name)
|
||||
if not self.language_id:
|
||||
raise ValidationError(msg='Invalid language selected')
|
||||
books = self.parse_csv_file(self.books_file, Book)
|
||||
self.wizard.progress_bar.setValue(0)
|
||||
self.wizard.progress_bar.setMinimum(0)
|
||||
self.wizard.progress_bar.setMaximum(len(books))
|
||||
book_list = self.process_books(books)
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
verses = self.parse_csv_file(self.verses_file, Verse)
|
||||
self.wizard.progress_bar.setValue(0)
|
||||
self.wizard.progress_bar.setMaximum(len(books) + 1)
|
||||
self.process_verses(verses, book_list)
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
except ValidationError:
|
||||
log.exception('Could not import CSV bible')
|
||||
return False
|
||||
return True
|
@ -34,6 +34,7 @@ from openlp.core.common import Registry, RegistryProperties, translate
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.lib.webpagereader import get_web_page
|
||||
from openlp.plugins.bibles.lib import SearchResults
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book
|
||||
|
||||
CLEANER_REGEX = re.compile(r' |<br />|\'\+\'')
|
||||
@ -576,10 +577,10 @@ class CWExtract(RegistryProperties):
|
||||
return bibles
|
||||
|
||||
|
||||
class HTTPBible(BibleDB, RegistryProperties):
|
||||
class HTTPBible(BibleImport, RegistryProperties):
|
||||
log.info('{name} HTTPBible loaded'.format(name=__name__))
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Finds all the bibles defined for the system. Creates an Interface Object for each bible containing connection
|
||||
information.
|
||||
@ -588,7 +589,7 @@ class HTTPBible(BibleDB, RegistryProperties):
|
||||
|
||||
Init confirms the bible exists and stores the database path.
|
||||
"""
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.download_source = kwargs['download_source']
|
||||
self.download_name = kwargs['download_name']
|
||||
# TODO: Clean up proxy stuff. We probably want one global proxy per connection type (HTTP and HTTPS) at most.
|
||||
@ -638,12 +639,8 @@ class HTTPBible(BibleDB, RegistryProperties):
|
||||
return False
|
||||
self.wizard.progress_bar.setMaximum(len(books) + 2)
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.HTTPBible', 'Registering Language...'))
|
||||
if self.language_id:
|
||||
self.save_meta('language_id', self.language_id)
|
||||
else:
|
||||
self.language_id = self.get_language(bible_name)
|
||||
self.language_id = self.get_language_id(bible_name=bible_name)
|
||||
if not self.language_id:
|
||||
log.error('Importing books from {name} failed'.format(name=self.filename))
|
||||
return False
|
||||
for book in books:
|
||||
if self.stop_import_flag:
|
@ -25,25 +25,17 @@ from lxml import etree, objectify
|
||||
|
||||
from openlp.core.common import translate, trace_error_handler
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenSongBible(BibleDB):
|
||||
class OpenSongBible(BibleImport):
|
||||
"""
|
||||
OpenSong Bible format importer class.
|
||||
OpenSong Bible format importer class. This class is used to import Bibles from OpenSong's XML format.
|
||||
"""
|
||||
def __init__(self, parent, **kwargs):
|
||||
"""
|
||||
Constructor to create and set up an instance of the OpenSongBible class. This class is used to import Bibles
|
||||
from OpenSong's XML format.
|
||||
"""
|
||||
log.debug(self.__class__.__name__)
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
self.filename = kwargs['filename']
|
||||
|
||||
def get_text(self, element):
|
||||
"""
|
||||
Recursively get all text in an objectify element and its child elements.
|
||||
@ -64,16 +56,9 @@ class OpenSongBible(BibleDB):
|
||||
Loads a Bible from file.
|
||||
"""
|
||||
log.debug('Starting OpenSong import from "{name}"'.format(name=self.filename))
|
||||
if not isinstance(self.filename, str):
|
||||
self.filename = str(self.filename, 'utf8')
|
||||
import_file = None
|
||||
success = True
|
||||
try:
|
||||
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
||||
# detection, and the two mechanisms together interfere with each other.
|
||||
import_file = open(self.filename, 'rb')
|
||||
opensong = objectify.parse(import_file)
|
||||
bible = opensong.getroot()
|
||||
bible = self.parse_xml(self.filename, use_objectify=True)
|
||||
# Check that we're not trying to import a Zefania XML bible, it is sometimes refered to as 'OpenSong'
|
||||
if bible.tag.upper() == 'XMLBIBLE':
|
||||
critical_error_message_box(
|
||||
@ -82,9 +67,8 @@ class OpenSongBible(BibleDB):
|
||||
'please use the Zefania import option.'))
|
||||
return False
|
||||
# No language info in the opensong format, so ask the user
|
||||
language_id = self.get_language(bible_name)
|
||||
language_id = self.get_language_id(bible_name=self.filename)
|
||||
if not language_id:
|
||||
log.error('Importing books from "{name}" failed'.format(name=self.filename))
|
||||
return False
|
||||
for book in bible.b:
|
||||
if self.stop_import_flag:
|
||||
@ -138,9 +122,6 @@ class OpenSongBible(BibleDB):
|
||||
except (IOError, AttributeError):
|
||||
log.exception('Loading Bible from OpenSong file failed')
|
||||
success = False
|
||||
finally:
|
||||
if import_file:
|
||||
import_file.close()
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
else:
|
@ -23,105 +23,87 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
from openlp.core.common import languages, translate, trace_error_handler
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
from openlp.core.common import translate, trace_error_handler
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
NS = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'}
|
||||
# Tags we don't use and can remove the content
|
||||
REMOVABLE_ELEMENTS = (
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}note',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}title',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}index',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'
|
||||
)
|
||||
# Tags we don't use but need to keep the content
|
||||
REMOVABLE_TAGS = (
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}p',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}l',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}lg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}q',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}a',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}w',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}divineName',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}foreign',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}hi',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}inscription',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}mentioned',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}name',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}reference',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}seg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}transChange',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}salute',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}signed',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}closer',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}speech',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}speaker',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}list',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}item',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}table',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}head',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}row',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}cell',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}caption'
|
||||
)
|
||||
|
||||
|
||||
def replacement(match):
|
||||
return match.group(2).upper()
|
||||
|
||||
|
||||
class OSISBible(BibleDB):
|
||||
class OSISBible(BibleImport):
|
||||
"""
|
||||
`OSIS <http://www.bibletechnologies.net/>`_ Bible format importer class.
|
||||
"""
|
||||
log.info('BibleOSISImpl loaded')
|
||||
|
||||
def __init__(self, parent, **kwargs):
|
||||
log.debug(self.__class__.__name__)
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
self.filename = kwargs['filename']
|
||||
|
||||
def do_import(self, bible_name=None):
|
||||
"""
|
||||
Loads a Bible from file.
|
||||
"""
|
||||
log.debug('Starting OSIS import from "{name}"'.format(name=self.filename))
|
||||
if not isinstance(self.filename, str):
|
||||
self.filename = str(self.filename, 'utf8')
|
||||
import_file = None
|
||||
success = True
|
||||
try:
|
||||
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
||||
# detection, and the two mechanisms together interfere with each other.
|
||||
import_file = open(self.filename, 'rb')
|
||||
osis_bible_tree = etree.parse(import_file, parser=etree.XMLParser(recover=True))
|
||||
namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'}
|
||||
# Find bible language
|
||||
language_id = None
|
||||
lang = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=namespace)
|
||||
if lang:
|
||||
language = languages.get_language(lang[0])
|
||||
if hasattr(language, 'id'):
|
||||
language_id = language.id
|
||||
# The language couldn't be detected, ask the user
|
||||
if not language_id:
|
||||
language_id = self.get_language(bible_name)
|
||||
if not language_id:
|
||||
log.error('Importing books from "{name}" failed'.format(name=self.filename))
|
||||
return False
|
||||
self.save_meta('language_id', language_id)
|
||||
num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace))
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport',
|
||||
'Removing unused tags (this may take a few minutes)...'))
|
||||
# We strip unused tags from the XML, this should leave us with only chapter, verse and div tags.
|
||||
# Strip tags we don't use - remove content
|
||||
etree.strip_elements(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}note',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}title',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}index',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'),
|
||||
with_tail=False)
|
||||
# Strip tags we don't use - keep content
|
||||
etree.strip_tags(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}p',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}l',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}lg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}q',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}a',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}w',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}divineName',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}foreign',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}hi',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}inscription',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}mentioned',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}name',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}reference',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}seg',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}transChange',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}salute',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}signed',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}closer',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}speech',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}speaker',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}list',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}item',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}table',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}head',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}row',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}cell',
|
||||
'{http://www.bibletechnologies.net/2003/OSIS/namespace}caption'))
|
||||
osis_bible_tree = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
|
||||
# Find bible language]
|
||||
language = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=NS)
|
||||
language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename)
|
||||
if not language_id:
|
||||
return False
|
||||
num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=NS))
|
||||
# Precompile a few xpath-querys
|
||||
verse_in_chapter = etree.XPath('count(//ns:chapter[1]/ns:verse)', namespaces=namespace)
|
||||
text_in_verse = etree.XPath('count(//ns:verse[1]/text())', namespaces=namespace)
|
||||
verse_in_chapter = etree.XPath('count(//ns:chapter[1]/ns:verse)', namespaces=NS)
|
||||
text_in_verse = etree.XPath('count(//ns:verse[1]/text())', namespaces=NS)
|
||||
# Find books in the bible
|
||||
bible_books = osis_bible_tree.xpath("//ns:div[@type='book']", namespaces=namespace)
|
||||
bible_books = osis_bible_tree.xpath("//ns:div[@type='book']", namespaces=NS)
|
||||
for book in bible_books:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
@ -189,9 +171,6 @@ class OSISBible(BibleDB):
|
||||
critical_error_message_box(message=translate('BiblesPlugin.OsisImport',
|
||||
'The file is not a valid OSIS-XML file:'
|
||||
'\n{text}').format(text=e.msg))
|
||||
finally:
|
||||
if import_file:
|
||||
import_file.close()
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
else:
|
@ -23,25 +23,26 @@
|
||||
import logging
|
||||
from pysword import modules
|
||||
|
||||
from openlp.core.common import languages, translate
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SwordBible(BibleDB):
|
||||
class SwordBible(BibleImport):
|
||||
"""
|
||||
SWORD Bible format importer class.
|
||||
"""
|
||||
def __init__(self, parent, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Constructor to create and set up an instance of the SwordBible class. This class is used to import Bibles
|
||||
from SWORD bible modules.
|
||||
"""
|
||||
log.debug(self.__class__.__name__)
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.sword_key = kwargs['sword_key']
|
||||
self.sword_path = kwargs['sword_path']
|
||||
if self.sword_path == '':
|
||||
@ -57,13 +58,11 @@ class SwordBible(BibleDB):
|
||||
pysword_modules = modules.SwordModules(self.sword_path)
|
||||
pysword_module_json = pysword_modules.parse_modules()[self.sword_key]
|
||||
bible = pysword_modules.get_bible_from_module(self.sword_key)
|
||||
language_id = None
|
||||
language = pysword_module_json['lang']
|
||||
language = language[language.find('.') + 1:]
|
||||
language = languages.get_language(language)
|
||||
if hasattr(language, 'id'):
|
||||
language_id = language.id
|
||||
self.save_meta('language_id', language_id)
|
||||
language_id = self.get_language_id(language, bible_name=self.filename)
|
||||
if not language_id:
|
||||
return False
|
||||
books = bible.get_structure().get_books()
|
||||
# Count number of books
|
||||
num_books = 0
|
@ -21,64 +21,41 @@
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
from openlp.core.common import languages, translate
|
||||
from openlp.core.common import translate
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Tags we don't use and can remove the content
|
||||
REMOVABLE_ELEMENTS = ('PROLOG', 'REMARK', 'CAPTION', 'MEDIA')
|
||||
# Tags we don't use but need to keep the content
|
||||
REMOVABLE_TAGS = ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF')
|
||||
|
||||
class ZefaniaBible(BibleDB):
|
||||
|
||||
class ZefaniaBible(BibleImport):
|
||||
"""
|
||||
Zefania Bible format importer class.
|
||||
Zefania Bible format importer class. This class is used to import Bibles from ZefaniaBible's XML format.
|
||||
"""
|
||||
def __init__(self, parent, **kwargs):
|
||||
"""
|
||||
Constructor to create and set up an instance of the ZefaniaBible class. This class is used to import Bibles
|
||||
from ZefaniaBible's XML format.
|
||||
"""
|
||||
log.debug(self.__class__.__name__)
|
||||
BibleDB.__init__(self, parent, **kwargs)
|
||||
self.filename = kwargs['filename']
|
||||
|
||||
def do_import(self, bible_name=None):
|
||||
"""
|
||||
Loads a Bible from file.
|
||||
"""
|
||||
log.debug('Starting Zefania import from "{name}"'.format(name=self.filename))
|
||||
if not isinstance(self.filename, str):
|
||||
self.filename = str(self.filename, 'utf8')
|
||||
import_file = None
|
||||
success = True
|
||||
try:
|
||||
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
||||
# detection, and the two mechanisms together interfere with each other.
|
||||
import_file = open(self.filename, 'rb')
|
||||
zefania_bible_tree = etree.parse(import_file, parser=etree.XMLParser(recover=True))
|
||||
xmlbible = self.parse_xml(self.filename, elements=REMOVABLE_ELEMENTS, tags=REMOVABLE_TAGS)
|
||||
# Find bible language
|
||||
language_id = None
|
||||
language = zefania_bible_tree.xpath("/XMLBIBLE/INFORMATION/language/text()")
|
||||
if language:
|
||||
language = languages.get_language(language[0])
|
||||
if hasattr(language, 'id'):
|
||||
language_id = language.id
|
||||
# The language couldn't be detected, ask the user
|
||||
language = xmlbible.xpath("/XMLBIBLE/INFORMATION/language/text()")
|
||||
language_id = self.get_language_id(language[0] if language else None, bible_name=self.filename)
|
||||
if not language_id:
|
||||
language_id = self.get_language(bible_name)
|
||||
if not language_id:
|
||||
log.error('Importing books from "{name}" failed'.format(name=self.filename))
|
||||
return False
|
||||
self.save_meta('language_id', language_id)
|
||||
num_books = int(zefania_bible_tree.xpath('count(//BIBLEBOOK)'))
|
||||
self.wizard.progress_bar.setMaximum(int(zefania_bible_tree.xpath('count(//CHAPTER)')))
|
||||
# Strip tags we don't use - keep content
|
||||
etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF'))
|
||||
# Strip tags we don't use - remove content
|
||||
etree.strip_elements(zefania_bible_tree, ('PROLOG', 'REMARK', 'CAPTION', 'MEDIA'), with_tail=False)
|
||||
xmlbible = zefania_bible_tree.getroot()
|
||||
num_books = int(xmlbible.xpath('count(//BIBLEBOOK)'))
|
||||
self.wizard.progress_bar.setMaximum(int(xmlbible.xpath('count(//CHAPTER)')))
|
||||
for BIBLEBOOK in xmlbible:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
@ -116,9 +93,6 @@ class ZefaniaBible(BibleDB):
|
||||
'compressed. You must decompress them before import.'))
|
||||
log.exception(str(e))
|
||||
success = False
|
||||
finally:
|
||||
if import_file:
|
||||
import_file.close()
|
||||
if self.stop_import_flag:
|
||||
return False
|
||||
else:
|
@ -26,13 +26,13 @@ import os
|
||||
from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file, UiStrings
|
||||
from openlp.plugins.bibles.lib import parse_reference, LanguageSelection
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta
|
||||
from .csvbible import CSVBible
|
||||
from .http import HTTPBible
|
||||
from .opensong import OpenSongBible
|
||||
from .osis import OSISBible
|
||||
from .zefania import ZefaniaBible
|
||||
from .importers.csvbible import CSVBible
|
||||
from .importers.http import HTTPBible
|
||||
from .importers.opensong import OpenSongBible
|
||||
from .importers.osis import OSISBible
|
||||
from .importers.zefania import ZefaniaBible
|
||||
try:
|
||||
from .sword import SwordBible
|
||||
from .importers.sword import SwordBible
|
||||
except:
|
||||
pass
|
||||
|
||||
@ -124,7 +124,6 @@ class BibleManager(RegistryProperties):
|
||||
files.remove('alternative_book_names.sqlite')
|
||||
log.debug('Bible Files {text}'.format(text=files))
|
||||
self.db_cache = {}
|
||||
self.old_bible_databases = []
|
||||
for filename in files:
|
||||
bible = BibleDB(self.parent, path=self.path, file=filename)
|
||||
if not bible.session:
|
||||
|
@ -133,7 +133,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
disable_optical_button_text = True
|
||||
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||
optical_button_tooltip = translate('MediaPlugin.MediaItem',
|
||||
'CD/DVD Playback is only supported if VLC is installed and enabled.')
|
||||
'CD/DVD playback is only supported if VLC is installed and enabled.')
|
||||
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=self.optical_icon,
|
||||
text=optical_button_text,
|
||||
tooltip=optical_button_tooltip,
|
||||
|
@ -125,11 +125,11 @@ class PresentationTab(SettingsTab):
|
||||
translate('PresentationPlugin.PresentationTab', 'Allow presentation application to be overridden'))
|
||||
self.ppt_slide_click_check_box.setText(
|
||||
translate('PresentationPlugin.PresentationTab',
|
||||
'Clicking on current slide advances to the next effect'))
|
||||
'Clicking on the current slide advances to the next effect'))
|
||||
self.ppt_window_check_box.setText(
|
||||
translate('PresentationPlugin.PresentationTab',
|
||||
'Let PowerPoint control the size and monitor of the presentations\n'
|
||||
'(This may fixes PowerPoint scaling issues in Windows 8 and 10)'))
|
||||
'(This may fix PowerPoint scaling issues in Windows 8 and 10)'))
|
||||
self.pdf_program_check_box.setText(
|
||||
translate('PresentationPlugin.PresentationTab', 'Use given full path for mudraw or ghostscript binary:'))
|
||||
|
||||
|
@ -317,11 +317,12 @@ class HttpRouter(RegistryProperties):
|
||||
Translate various strings in the mobile app.
|
||||
"""
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
self.template_vars = {
|
||||
'app_title': "{remote} | OpenLP".format(remote=remote),
|
||||
'stage_title': "{stage} | OpenLP".format(stage=stage),
|
||||
'live_title': "{live} | OpenLP".format(live=UiStrings().Live),
|
||||
'app_title': "{main} {remote}".format(main=UiStrings().OLPV2x, remote=remote),
|
||||
'stage_title': "{main} {stage}".format(main=UiStrings().OLPV2x, stage=stage),
|
||||
'live_title': "{main} {live}".format(main=UiStrings().OLPV2x, live=live),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
||||
|
@ -31,9 +31,8 @@ from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, CONTROL_CHARS
|
||||
from openlp.core.lib import translate
|
||||
from openlp.plugins.songs.lib.db import MediaFile, Song
|
||||
from .db import Author
|
||||
from .ui import SongStrings
|
||||
from openlp.plugins.songs.lib.db import Author, MediaFile, Song, Topic
|
||||
from openlp.plugins.songs.lib.ui import SongStrings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -315,8 +314,8 @@ def retrieve_windows_encoding(recommendation=None):
|
||||
]
|
||||
recommended_index = -1
|
||||
if recommendation:
|
||||
for index in range(len(encodings)):
|
||||
if recommendation == encodings[index][0]:
|
||||
for index, encoding in enumerate(encodings):
|
||||
if recommendation == encoding[0]:
|
||||
recommended_index = index
|
||||
break
|
||||
if recommended_index > -1:
|
||||
@ -442,7 +441,7 @@ def strip_rtf(text, default_encoding=None):
|
||||
# Encoded buffer.
|
||||
ebytes = bytearray()
|
||||
for match in PATTERN.finditer(text):
|
||||
iinu, word, arg, hex, char, brace, tchar = match.groups()
|
||||
iinu, word, arg, hex_, char, brace, tchar = match.groups()
|
||||
# \x (non-alpha character)
|
||||
if char:
|
||||
if char in '\\{}':
|
||||
@ -450,7 +449,7 @@ def strip_rtf(text, default_encoding=None):
|
||||
else:
|
||||
word = char
|
||||
# Flush encoded buffer to output buffer
|
||||
if ebytes and not hex and not tchar:
|
||||
if ebytes and not hex_ and not tchar:
|
||||
failed = False
|
||||
while True:
|
||||
try:
|
||||
@ -507,11 +506,11 @@ def strip_rtf(text, default_encoding=None):
|
||||
elif iinu:
|
||||
ignorable = True
|
||||
# \'xx
|
||||
elif hex:
|
||||
elif hex_:
|
||||
if curskip > 0:
|
||||
curskip -= 1
|
||||
elif not ignorable:
|
||||
ebytes.append(int(hex, 16))
|
||||
ebytes.append(int(hex_, 16))
|
||||
elif tchar:
|
||||
if curskip > 0:
|
||||
curskip -= 1
|
||||
|
@ -23,7 +23,8 @@
|
||||
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
import random
|
||||
import re
|
||||
from http.cookiejar import CookieJar
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import HTTPCookieProcessor, URLError, build_opener
|
||||
@ -32,14 +33,21 @@ from html import unescape
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
|
||||
from openlp.plugins.songs.lib import Song, VerseType, clean_song, Author
|
||||
from openlp.plugins.songs.lib import Song, Author, Topic, VerseType, clean_song
|
||||
from openlp.plugins.songs.lib.openlyricsxml import SongXML
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \
|
||||
'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \
|
||||
'Mobile Safari/534.30'
|
||||
BASE_URL = 'https://mobile.songselect.com'
|
||||
LOGIN_URL = BASE_URL + '/account/login'
|
||||
USER_AGENTS = [
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/52.0.2743.116 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0'
|
||||
]
|
||||
BASE_URL = 'https://songselect.ccli.com'
|
||||
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
|
||||
'https%3a%2f%2fsongselect.ccli.com%2f'
|
||||
LOGIN_URL = 'https://profile.ccli.com/'
|
||||
LOGOUT_URL = BASE_URL + '/account/logout'
|
||||
SEARCH_URL = BASE_URL + '/search/results'
|
||||
|
||||
@ -60,7 +68,7 @@ class SongSelectImport(object):
|
||||
self.db_manager = db_manager
|
||||
self.html_parser = HTMLParser()
|
||||
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
|
||||
self.opener.addheaders = [('User-Agent', USER_AGENT)]
|
||||
self.opener.addheaders = [('User-Agent', random.choice(USER_AGENTS))]
|
||||
self.run_search = True
|
||||
|
||||
def login(self, username, password, callback=None):
|
||||
@ -76,27 +84,27 @@ class SongSelectImport(object):
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml')
|
||||
except (TypeError, URLError) as e:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=e))
|
||||
login_page = BeautifulSoup(self.opener.open(LOGIN_PAGE).read(), 'lxml')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=error))
|
||||
return False
|
||||
if callback:
|
||||
callback()
|
||||
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
data = urlencode({
|
||||
'__RequestVerificationToken': token_input['value'],
|
||||
'UserName': username,
|
||||
'Password': password,
|
||||
'emailAddress': username,
|
||||
'password': password,
|
||||
'RememberMe': 'false'
|
||||
})
|
||||
try:
|
||||
posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml')
|
||||
except (TypeError, URLError) as e:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=e))
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not login to SongSelect, {error}'.format(error=error))
|
||||
return False
|
||||
if callback:
|
||||
callback()
|
||||
return not posted_page.find('input', attrs={'name': '__RequestVerificationToken'})
|
||||
return posted_page.find('input', id='SearchText') is not None
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
@ -104,8 +112,8 @@ class SongSelectImport(object):
|
||||
"""
|
||||
try:
|
||||
self.opener.open(LOGOUT_URL)
|
||||
except (TypeError, URLError) as e:
|
||||
log.exception('Could not log of SongSelect, {error}'.format(error=e))
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not log of SongSelect, {error}'.format(error=error))
|
||||
|
||||
def search(self, search_text, max_results, callback=None):
|
||||
"""
|
||||
@ -117,7 +125,15 @@ class SongSelectImport(object):
|
||||
:return: List of songs
|
||||
"""
|
||||
self.run_search = True
|
||||
params = {'allowredirect': 'false', 'SearchTerm': search_text}
|
||||
params = {
|
||||
'SongContent': '',
|
||||
'PrimaryLanguage': '',
|
||||
'Keys': '',
|
||||
'Themes': '',
|
||||
'List': '',
|
||||
'Sort': '',
|
||||
'SearchText': search_text
|
||||
}
|
||||
current_page = 1
|
||||
songs = []
|
||||
while self.run_search:
|
||||
@ -125,17 +141,17 @@ class SongSelectImport(object):
|
||||
params['page'] = current_page
|
||||
try:
|
||||
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
|
||||
search_results = results_page.find_all('li', 'result pane')
|
||||
except (TypeError, URLError) as e:
|
||||
log.exception('Could not search SongSelect, {error}'.format(error=e))
|
||||
search_results = results_page.find_all('div', 'song-result')
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not search SongSelect, {error}'.format(error=error))
|
||||
search_results = None
|
||||
if not search_results:
|
||||
break
|
||||
for result in search_results:
|
||||
song = {
|
||||
'title': unescape(result.find('h3').string),
|
||||
'authors': [unescape(author.string) for author in result.find_all('li')],
|
||||
'link': BASE_URL + result.find('a')['href']
|
||||
'title': unescape(result.find('p', 'song-result-title').find('a').string).strip(),
|
||||
'authors': unescape(result.find('p', 'song-result-subtitle').string).strip().split(', '),
|
||||
'link': BASE_URL + result.find('p', 'song-result-title').find('a')['href']
|
||||
}
|
||||
if callback:
|
||||
callback(song)
|
||||
@ -157,33 +173,43 @@ class SongSelectImport(object):
|
||||
callback()
|
||||
try:
|
||||
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
|
||||
except (TypeError, URLError) as e:
|
||||
log.exception('Could not get song from SongSelect, {error}'.format(error=e))
|
||||
except (TypeError, URLError) as error:
|
||||
log.exception('Could not get song from SongSelect, {error}'.format(error=error))
|
||||
return None
|
||||
if callback:
|
||||
callback()
|
||||
try:
|
||||
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
|
||||
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/viewlyrics').read(), 'lxml')
|
||||
except (TypeError, URLError):
|
||||
log.exception('Could not get lyrics from SongSelect')
|
||||
return None
|
||||
if callback:
|
||||
callback()
|
||||
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
|
||||
song['copyright'] = unescape(song['copyright'])
|
||||
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
|
||||
copyright_elements = []
|
||||
theme_elements = []
|
||||
copyrights_regex = re.compile(r'\bCopyrights\b')
|
||||
themes_regex = re.compile(r'\bThemes\b')
|
||||
for ul in song_page.find_all('ul', 'song-meta-list'):
|
||||
if ul.find('li', string=copyrights_regex):
|
||||
copyright_elements.extend(ul.find_all('li')[1:])
|
||||
if ul.find('li', string=themes_regex):
|
||||
theme_elements.extend(ul.find_all('li')[1:])
|
||||
song['copyright'] = '/'.join([unescape(li.string).strip() for li in copyright_elements])
|
||||
song['topics'] = [unescape(li.string).strip() for li in theme_elements]
|
||||
song['ccli_number'] = song_page.find('div', 'song-content-data').find('ul').find('li')\
|
||||
.find('strong').string.strip()
|
||||
song['verses'] = []
|
||||
verses = lyrics_page.find('section', 'lyrics').find_all('p')
|
||||
verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3')
|
||||
for counter in range(len(verses)):
|
||||
verse = {'label': verse_labels[counter].string, 'lyrics': ''}
|
||||
for v in verses[counter].contents:
|
||||
verses = lyrics_page.find('div', 'song-viewer lyrics').find_all('p')
|
||||
verse_labels = lyrics_page.find('div', 'song-viewer lyrics').find_all('h3')
|
||||
for verse, label in zip(verses, verse_labels):
|
||||
song_verse = {'label': unescape(label.string).strip(), 'lyrics': ''}
|
||||
for v in verse.contents:
|
||||
if isinstance(v, NavigableString):
|
||||
verse['lyrics'] = verse['lyrics'] + v.string
|
||||
song_verse['lyrics'] += unescape(v.string).strip()
|
||||
else:
|
||||
verse['lyrics'] += '\n'
|
||||
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
|
||||
song['verses'].append(unescape(verse))
|
||||
song_verse['lyrics'] += '\n'
|
||||
song_verse['lyrics'] = song_verse['lyrics'].strip(' \n\r\t')
|
||||
song['verses'].append(song_verse)
|
||||
for counter, author in enumerate(song['authors']):
|
||||
song['authors'][counter] = unescape(author)
|
||||
return song
|
||||
@ -199,7 +225,11 @@ class SongSelectImport(object):
|
||||
song_xml = SongXML()
|
||||
verse_order = []
|
||||
for verse in song['verses']:
|
||||
verse_type, verse_number = verse['label'].split(' ')[:2]
|
||||
if ' ' in verse['label']:
|
||||
verse_type, verse_number = verse['label'].split(' ', 1)
|
||||
else:
|
||||
verse_type = verse['label']
|
||||
verse_number = 1
|
||||
verse_type = VerseType.from_loose_input(verse_type)
|
||||
verse_number = int(verse_number)
|
||||
song_xml.add_verse_to_lyrics(VerseType.tags[verse_type], verse_number, verse['lyrics'])
|
||||
@ -220,6 +250,11 @@ class SongSelectImport(object):
|
||||
last_name = name_parts[1]
|
||||
author = Author.populate(first_name=first_name, last_name=last_name, display_name=author_name)
|
||||
db_song.add_author(author)
|
||||
for topic_name in song.get('topics', []):
|
||||
topic = self.db_manager.get_object_filtered(Topic, Topic.name == topic_name)
|
||||
if not topic:
|
||||
topic = Topic.populate(name=topic_name)
|
||||
db_song.topics.append(topic)
|
||||
self.db_manager.save_object(db_song)
|
||||
return db_song
|
||||
|
||||
|
@ -23,11 +23,12 @@
|
||||
Functional tests to test the AppLocation class and related methods.
|
||||
"""
|
||||
import os
|
||||
from io import BytesIO
|
||||
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 openlp.core.common import add_actions, clean_filename, delete_file, get_file_encoding, get_filesystem_encoding, \
|
||||
get_uno_command, get_uno_instance, split_filename
|
||||
from tests.functional import MagicMock, PropertyMock, call, patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
@ -340,3 +341,63 @@ class TestInit(TestCase, TestMixin):
|
||||
# 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 test_get_file_name_encoding_done_test(self):
|
||||
"""
|
||||
Test get_file_encoding when the detector sets done to True
|
||||
"""
|
||||
# GIVEN: A mocked UniversalDetector instance with done attribute set to True after first iteration
|
||||
with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
|
||||
patch('builtins.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
|
||||
encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
|
||||
mocked_universal_detector_inst = MagicMock(result=encoding_result)
|
||||
type(mocked_universal_detector_inst).done = PropertyMock(side_effect=[False, True])
|
||||
mocked_universal_detector.return_value = mocked_universal_detector_inst
|
||||
|
||||
# WHEN: Calling get_file_encoding
|
||||
result = get_file_encoding('file name')
|
||||
|
||||
# THEN: The feed method of UniversalDetector should only br called once before returning a result
|
||||
mocked_open.assert_called_once_with('file name', 'rb')
|
||||
self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256)])
|
||||
mocked_universal_detector_inst.close.assert_called_once_with()
|
||||
self.assertEqual(result, encoding_result)
|
||||
|
||||
def test_get_file_name_encoding_eof_test(self):
|
||||
"""
|
||||
Test get_file_encoding when the end of the file is reached
|
||||
"""
|
||||
# GIVEN: A mocked UniversalDetector instance which isn't set to done and a mocked open, with 1040 bytes of test
|
||||
# data (enough to run the iterator twice)
|
||||
with patch('openlp.core.common.UniversalDetector') as mocked_universal_detector, \
|
||||
patch('builtins.open', return_value=BytesIO(b"data" * 260)) as mocked_open:
|
||||
encoding_result = {'encoding': 'UTF-8', 'confidence': 0.99}
|
||||
mocked_universal_detector_inst = MagicMock(mock=mocked_universal_detector,
|
||||
**{'done': False, 'result': encoding_result})
|
||||
mocked_universal_detector.return_value = mocked_universal_detector_inst
|
||||
|
||||
# WHEN: Calling get_file_encoding
|
||||
result = get_file_encoding('file name')
|
||||
|
||||
# THEN: The feed method of UniversalDetector should have been called twice before returning a result
|
||||
mocked_open.assert_called_once_with('file name', 'rb')
|
||||
self.assertEqual(mocked_universal_detector_inst.feed.mock_calls, [call(b"data" * 256), call(b"data" * 4)])
|
||||
mocked_universal_detector_inst.close.assert_called_once_with()
|
||||
self.assertEqual(result, encoding_result)
|
||||
|
||||
def test_get_file_name_encoding_oserror_test(self):
|
||||
"""
|
||||
Test get_file_encoding when the end of the file is reached
|
||||
"""
|
||||
# GIVEN: A mocked UniversalDetector instance which isn't set to done and a mocked open, with 1040 bytes of test
|
||||
# data (enough to run the iterator twice)
|
||||
with patch('openlp.core.common.UniversalDetector'), \
|
||||
patch('builtins.open', side_effect=OSError), \
|
||||
patch('openlp.core.common.log') as mocked_log:
|
||||
|
||||
# WHEN: Calling get_file_encoding
|
||||
result = get_file_encoding('file name')
|
||||
|
||||
# THEN: log.exception should be called and get_file_encoding should return None
|
||||
mocked_log.exception.assert_called_once_with('Error detecting file encoding')
|
||||
self.assertIsNone(result)
|
||||
|
@ -75,3 +75,24 @@ class TestRegistryProperties(TestCase, RegistryProperties):
|
||||
|
||||
# THEN the application should be none
|
||||
self.assertEqual(self.application, application, 'The application value should match')
|
||||
|
||||
@patch('openlp.core.common.registryproperties.is_win')
|
||||
def test_get_application_on_windows(self, mocked_is_win):
|
||||
"""
|
||||
Set that getting the application object on Windows happens dynamically
|
||||
"""
|
||||
# GIVEN an Empty Registry and we're on Windows
|
||||
mocked_is_win.return_value = True
|
||||
mock_application = MagicMock()
|
||||
reg_props = RegistryProperties()
|
||||
registry = Registry()
|
||||
|
||||
# WHEN the application is accessed
|
||||
with patch.object(registry, 'get') as mocked_get:
|
||||
mocked_get.return_value = mock_application
|
||||
actual_application = reg_props.application
|
||||
|
||||
# THEN the application should be the mock object, and the correct function should have been called
|
||||
self.assertEqual(mock_application, actual_application, 'The application value should match')
|
||||
mocked_is_win.assert_called_with()
|
||||
mocked_get.assert_called_with('application')
|
||||
|
@ -243,7 +243,7 @@ class TestSlideController(TestCase):
|
||||
mocked_service_item = MagicMock()
|
||||
mocked_service_item.from_service = False
|
||||
mocked_preview_widget.current_slide_number.return_value = 1
|
||||
mocked_preview_widget.slide_count.return_value = 2
|
||||
mocked_preview_widget.slide_count = MagicMock(return_value=2)
|
||||
mocked_live_controller.preview_widget = MagicMock()
|
||||
Registry.create()
|
||||
Registry().register('live_controller', mocked_live_controller)
|
||||
@ -273,7 +273,7 @@ class TestSlideController(TestCase):
|
||||
mocked_service_item.from_service = True
|
||||
mocked_service_item.unique_identifier = 42
|
||||
mocked_preview_widget.current_slide_number.return_value = 1
|
||||
mocked_preview_widget.slide_count.return_value = 2
|
||||
mocked_preview_widget.slide_count = MagicMock(return_value=2)
|
||||
mocked_live_controller.preview_widget = MagicMock()
|
||||
Registry.create()
|
||||
Registry().register('live_controller', mocked_live_controller)
|
||||
|
529
tests/functional/openlp_core_ui_media/test_systemplayer.py
Normal file
529
tests/functional/openlp_core_ui_media/test_systemplayer.py
Normal file
@ -0,0 +1,529 @@
|
||||
# -*- 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.ui.media.systemplayer package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt5 import QtCore, QtMultimedia
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.ui.media import MediaState
|
||||
from openlp.core.ui.media.systemplayer import SystemPlayer, CheckMediaWorker, ADDITIONAL_EXT
|
||||
|
||||
from tests.functional import MagicMock, call, patch
|
||||
|
||||
|
||||
class TestSystemPlayer(TestCase):
|
||||
"""
|
||||
Test the system media player
|
||||
"""
|
||||
@patch('openlp.core.ui.media.systemplayer.mimetypes')
|
||||
@patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
|
||||
def test_constructor(self, MockQMediaPlayer, mocked_mimetypes):
|
||||
"""
|
||||
Test the SystemPlayer constructor
|
||||
"""
|
||||
# GIVEN: The SystemPlayer class and a mockedQMediaPlayer
|
||||
mocked_media_player = MagicMock()
|
||||
mocked_media_player.supportedMimeTypes.return_value = [
|
||||
'application/postscript',
|
||||
'audio/aiff',
|
||||
'audio/x-aiff',
|
||||
'text/html',
|
||||
'video/animaflex',
|
||||
'video/x-ms-asf'
|
||||
]
|
||||
mocked_mimetypes.guess_all_extensions.side_effect = [
|
||||
['.aiff'],
|
||||
['.aiff'],
|
||||
['.afl'],
|
||||
['.asf']
|
||||
]
|
||||
MockQMediaPlayer.return_value = mocked_media_player
|
||||
|
||||
# WHEN: An object is created from it
|
||||
player = SystemPlayer(self)
|
||||
|
||||
# THEN: The correct initial values should be set up
|
||||
self.assertEqual('system', player.name)
|
||||
self.assertEqual('System', player.original_name)
|
||||
self.assertEqual('&System', player.display_name)
|
||||
self.assertEqual(self, player.parent)
|
||||
self.assertEqual(ADDITIONAL_EXT, player.additional_extensions)
|
||||
MockQMediaPlayer.assert_called_once_with(None, QtMultimedia.QMediaPlayer.VideoSurface)
|
||||
mocked_mimetypes.init.assert_called_once_with()
|
||||
mocked_media_player.service.assert_called_once_with()
|
||||
mocked_media_player.supportedMimeTypes.assert_called_once_with()
|
||||
self.assertEqual(['*.aiff'], player.audio_extensions_list)
|
||||
self.assertEqual(['*.afl', '*.asf'], player.video_extensions_list)
|
||||
|
||||
@patch('openlp.core.ui.media.systemplayer.QtMultimediaWidgets.QVideoWidget')
|
||||
@patch('openlp.core.ui.media.systemplayer.QtMultimedia.QMediaPlayer')
|
||||
def test_setup(self, MockQMediaPlayer, MockQVideoWidget):
|
||||
"""
|
||||
Test the setup() method of SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mock display
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.size.return_value = [1, 2, 3, 4]
|
||||
mocked_video_widget = MagicMock()
|
||||
mocked_media_player = MagicMock()
|
||||
MockQVideoWidget.return_value = mocked_video_widget
|
||||
MockQMediaPlayer.return_value = mocked_media_player
|
||||
|
||||
# WHEN: setup() is run
|
||||
player.setup(mocked_display)
|
||||
|
||||
# THEN: The player should have a display widget
|
||||
MockQVideoWidget.assert_called_once_with(mocked_display)
|
||||
self.assertEqual(mocked_video_widget, mocked_display.video_widget)
|
||||
mocked_display.size.assert_called_once_with()
|
||||
mocked_video_widget.resize.assert_called_once_with([1, 2, 3, 4])
|
||||
MockQMediaPlayer.assert_called_with(mocked_display)
|
||||
self.assertEqual(mocked_media_player, mocked_display.media_player)
|
||||
mocked_media_player.setVideoOutput.assert_called_once_with(mocked_video_widget)
|
||||
mocked_video_widget.raise_.assert_called_once_with()
|
||||
mocked_video_widget.hide.assert_called_once_with()
|
||||
self.assertTrue(player.has_own_widget)
|
||||
|
||||
def test_check_available(self):
|
||||
"""
|
||||
Test the check_available() method on SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
|
||||
# WHEN: check_available is run
|
||||
result = player.check_available()
|
||||
|
||||
# THEN: it should be available
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_load_valid_media(self):
|
||||
"""
|
||||
Test the load() method of SystemPlayer with a valid media file
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.media_info.volume = 1
|
||||
mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
|
||||
|
||||
# WHEN: The load() method is run
|
||||
with patch.object(player, 'check_media') as mocked_check_media, \
|
||||
patch.object(player, 'volume') as mocked_volume:
|
||||
mocked_check_media.return_value = True
|
||||
result = player.load(mocked_display)
|
||||
|
||||
# THEN: the file is sent to the video widget
|
||||
mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
|
||||
mocked_check_media.assert_called_once_with('/path/to/file')
|
||||
mocked_display.media_player.setMedia.assert_called_once_with(
|
||||
QtMultimedia.QMediaContent(QtCore.QUrl.fromLocalFile('/path/to/file')))
|
||||
mocked_volume.assert_called_once_with(mocked_display, 1)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_load_invalid_media(self):
|
||||
"""
|
||||
Test the load() method of SystemPlayer with an invalid media file
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.media_info.volume = 1
|
||||
mocked_display.controller.media_info.file_info.absoluteFilePath.return_value = '/path/to/file'
|
||||
|
||||
# WHEN: The load() method is run
|
||||
with patch.object(player, 'check_media') as mocked_check_media, \
|
||||
patch.object(player, 'volume') as mocked_volume:
|
||||
mocked_check_media.return_value = False
|
||||
result = player.load(mocked_display)
|
||||
|
||||
# THEN: stuff
|
||||
mocked_display.controller.media_info.file_info.absoluteFilePath.assert_called_once_with()
|
||||
mocked_check_media.assert_called_once_with('/path/to/file')
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_resize(self):
|
||||
"""
|
||||
Test the resize() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.size.return_value = [1, 2, 3, 4]
|
||||
|
||||
# WHEN: The resize() method is called
|
||||
player.resize(mocked_display)
|
||||
|
||||
# THEN: The player is resized
|
||||
mocked_display.size.assert_called_once_with()
|
||||
mocked_display.video_widget.resize.assert_called_once_with([1, 2, 3, 4])
|
||||
|
||||
@patch('openlp.core.ui.media.systemplayer.functools')
|
||||
def test_play_is_live(self, mocked_functools):
|
||||
"""
|
||||
Test the play() method of the SystemPlayer on the live display
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
mocked_functools.partial.return_value = 'function'
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.is_live = True
|
||||
mocked_display.controller.media_info.start_time = 1
|
||||
mocked_display.controller.media_info.volume = 1
|
||||
|
||||
# WHEN: play() is called
|
||||
with patch.object(player, 'get_live_state') as mocked_get_live_state, \
|
||||
patch.object(player, 'seek') as mocked_seek, \
|
||||
patch.object(player, 'volume') as mocked_volume, \
|
||||
patch.object(player, 'set_state') as mocked_set_state:
|
||||
mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
|
||||
result = player.play(mocked_display)
|
||||
|
||||
# THEN: the media file is played
|
||||
mocked_get_live_state.assert_called_once_with()
|
||||
mocked_display.media_player.play.assert_called_once_with()
|
||||
mocked_seek.assert_called_once_with(mocked_display, 1000)
|
||||
mocked_volume.assert_called_once_with(mocked_display, 1)
|
||||
mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
|
||||
mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
|
||||
mocked_display.video_widget.raise_.assert_called_once_with()
|
||||
self.assertTrue(result)
|
||||
|
||||
@patch('openlp.core.ui.media.systemplayer.functools')
|
||||
def test_play_is_preview(self, mocked_functools):
|
||||
"""
|
||||
Test the play() method of the SystemPlayer on the preview display
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
mocked_functools.partial.return_value = 'function'
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.is_live = False
|
||||
mocked_display.controller.media_info.start_time = 1
|
||||
mocked_display.controller.media_info.volume = 1
|
||||
|
||||
# WHEN: play() is called
|
||||
with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
|
||||
patch.object(player, 'seek') as mocked_seek, \
|
||||
patch.object(player, 'volume') as mocked_volume, \
|
||||
patch.object(player, 'set_state') as mocked_set_state:
|
||||
mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PlayingState
|
||||
result = player.play(mocked_display)
|
||||
|
||||
# THEN: the media file is played
|
||||
mocked_get_preview_state.assert_called_once_with()
|
||||
mocked_display.media_player.play.assert_called_once_with()
|
||||
mocked_seek.assert_called_once_with(mocked_display, 1000)
|
||||
mocked_volume.assert_called_once_with(mocked_display, 1)
|
||||
mocked_display.media_player.durationChanged.connect.assert_called_once_with('function')
|
||||
mocked_set_state.assert_called_once_with(MediaState.Playing, mocked_display)
|
||||
mocked_display.video_widget.raise_.assert_called_once_with()
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_pause_is_live(self):
|
||||
"""
|
||||
Test the pause() method of the SystemPlayer on the live display
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.is_live = True
|
||||
|
||||
# WHEN: The pause method is called
|
||||
with patch.object(player, 'get_live_state') as mocked_get_live_state, \
|
||||
patch.object(player, 'set_state') as mocked_set_state:
|
||||
mocked_get_live_state.return_value = QtMultimedia.QMediaPlayer.PausedState
|
||||
player.pause(mocked_display)
|
||||
|
||||
# THEN: The video is paused
|
||||
mocked_display.media_player.pause.assert_called_once_with()
|
||||
mocked_get_live_state.assert_called_once_with()
|
||||
mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
|
||||
|
||||
def test_pause_is_preview(self):
|
||||
"""
|
||||
Test the pause() method of the SystemPlayer on the preview display
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.controller.is_live = False
|
||||
|
||||
# WHEN: The pause method is called
|
||||
with patch.object(player, 'get_preview_state') as mocked_get_preview_state, \
|
||||
patch.object(player, 'set_state') as mocked_set_state:
|
||||
mocked_get_preview_state.return_value = QtMultimedia.QMediaPlayer.PausedState
|
||||
player.pause(mocked_display)
|
||||
|
||||
# THEN: The video is paused
|
||||
mocked_display.media_player.pause.assert_called_once_with()
|
||||
mocked_get_preview_state.assert_called_once_with()
|
||||
mocked_set_state.assert_called_once_with(MediaState.Paused, mocked_display)
|
||||
|
||||
def test_stop(self):
|
||||
"""
|
||||
Test the stop() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
|
||||
# WHEN: The stop method is called
|
||||
with patch.object(player, 'set_visible') as mocked_set_visible, \
|
||||
patch.object(player, 'set_state') as mocked_set_state:
|
||||
player.stop(mocked_display)
|
||||
|
||||
# THEN: The video is stopped
|
||||
mocked_display.media_player.stop.assert_called_once_with()
|
||||
mocked_set_visible.assert_called_once_with(mocked_display, False)
|
||||
mocked_set_state.assert_called_once_with(MediaState.Stopped, mocked_display)
|
||||
|
||||
def test_volume(self):
|
||||
"""
|
||||
Test the volume() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.has_audio = True
|
||||
|
||||
# WHEN: The stop method is called
|
||||
player.volume(mocked_display, 2)
|
||||
|
||||
# THEN: The video is stopped
|
||||
mocked_display.media_player.setVolume.assert_called_once_with(2)
|
||||
|
||||
def test_seek(self):
|
||||
"""
|
||||
Test the seek() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
|
||||
# WHEN: The stop method is called
|
||||
player.seek(mocked_display, 2)
|
||||
|
||||
# THEN: The video is stopped
|
||||
mocked_display.media_player.setPosition.assert_called_once_with(2)
|
||||
|
||||
def test_reset(self):
|
||||
"""
|
||||
Test the reset() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
mocked_display = MagicMock()
|
||||
|
||||
# WHEN: reset() is called
|
||||
with patch.object(player, 'set_state') as mocked_set_state, \
|
||||
patch.object(player, 'set_visible') as mocked_set_visible:
|
||||
player.reset(mocked_display)
|
||||
|
||||
# THEN: The media player is reset
|
||||
mocked_display.media_player.stop()
|
||||
mocked_display.media_player.setMedia.assert_called_once_with(QtMultimedia.QMediaContent())
|
||||
mocked_set_visible.assert_called_once_with(mocked_display, False)
|
||||
mocked_display.video_widget.setVisible.assert_called_once_with(False)
|
||||
mocked_set_state.assert_called_once_with(MediaState.Off, mocked_display)
|
||||
|
||||
def test_set_visible(self):
|
||||
"""
|
||||
Test the set_visible() method on the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked display
|
||||
player = SystemPlayer(self)
|
||||
player.has_own_widget = True
|
||||
mocked_display = MagicMock()
|
||||
|
||||
# WHEN: set_visible() is called
|
||||
player.set_visible(mocked_display, True)
|
||||
|
||||
# THEN: The widget should be visible
|
||||
mocked_display.video_widget.setVisible.assert_called_once_with(True)
|
||||
|
||||
def test_set_duration(self):
|
||||
"""
|
||||
Test the set_duration() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: a mocked controller
|
||||
mocked_controller = MagicMock()
|
||||
mocked_controller.media_info.length = 5
|
||||
|
||||
# WHEN: The set_duration() is called. NB: the 10 here is ignored by the code
|
||||
SystemPlayer.set_duration(mocked_controller, 10)
|
||||
|
||||
# THEN: The maximum length of the slider should be set
|
||||
mocked_controller.seek_slider.setMaximum.assert_called_once_with(5)
|
||||
|
||||
def test_update_ui(self):
|
||||
"""
|
||||
Test the update_ui() method on the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
player.state = MediaState.Playing
|
||||
mocked_display = MagicMock()
|
||||
mocked_display.media_player.state.return_value = QtMultimedia.QMediaPlayer.PausedState
|
||||
mocked_display.controller.media_info.end_time = 1
|
||||
mocked_display.media_player.position.return_value = 2
|
||||
mocked_display.controller.seek_slider.isSliderDown.return_value = False
|
||||
|
||||
# WHEN: update_ui() is called
|
||||
with patch.object(player, 'stop') as mocked_stop, \
|
||||
patch.object(player, 'set_visible') as mocked_set_visible:
|
||||
player.update_ui(mocked_display)
|
||||
|
||||
# THEN: The UI is updated
|
||||
expected_stop_calls = [call(mocked_display), call(mocked_display)]
|
||||
expected_position_calls = [call(), call()]
|
||||
expected_block_signals_calls = [call(True), call(False)]
|
||||
mocked_display.media_player.state.assert_called_once_with()
|
||||
self.assertEqual(2, mocked_stop.call_count)
|
||||
self.assertEqual(expected_stop_calls, mocked_stop.call_args_list)
|
||||
self.assertEqual(2, mocked_display.media_player.position.call_count)
|
||||
self.assertEqual(expected_position_calls, mocked_display.media_player.position.call_args_list)
|
||||
mocked_set_visible.assert_called_once_with(mocked_display, False)
|
||||
mocked_display.controller.seek_slider.isSliderDown.assert_called_once_with()
|
||||
self.assertEqual(expected_block_signals_calls,
|
||||
mocked_display.controller.seek_slider.blockSignals.call_args_list)
|
||||
mocked_display.controller.seek_slider.setSliderPosition.assert_called_once_with(2)
|
||||
|
||||
def test_get_media_display_css(self):
|
||||
"""
|
||||
Test the get_media_display_css() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
|
||||
# WHEN: get_media_display_css() is called
|
||||
result = player.get_media_display_css()
|
||||
|
||||
# THEN: The css should be empty
|
||||
self.assertEqual('', result)
|
||||
|
||||
def test_get_info(self):
|
||||
"""
|
||||
Test the get_info() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance
|
||||
player = SystemPlayer(self)
|
||||
|
||||
# WHEN: get_info() is called
|
||||
result = player.get_info()
|
||||
|
||||
# THEN: The info should be correct
|
||||
expected_info = 'This media player uses your operating system to provide media capabilities.<br/> ' \
|
||||
'<strong>Audio</strong><br/>[]<br/><strong>Video</strong><br/>[]<br/>'
|
||||
self.assertEqual(expected_info, result)
|
||||
|
||||
@patch('openlp.core.ui.media.systemplayer.CheckMediaWorker')
|
||||
@patch('openlp.core.ui.media.systemplayer.QtCore.QThread')
|
||||
def test_check_media(self, MockQThread, MockCheckMediaWorker):
|
||||
"""
|
||||
Test the check_media() method of the SystemPlayer
|
||||
"""
|
||||
# GIVEN: A SystemPlayer instance and a mocked thread
|
||||
valid_file = '/path/to/video.ogv'
|
||||
mocked_application = MagicMock()
|
||||
Registry().create()
|
||||
Registry().register('application', mocked_application)
|
||||
player = SystemPlayer(self)
|
||||
mocked_thread = MagicMock()
|
||||
mocked_thread.isRunning.side_effect = [True, False]
|
||||
mocked_thread.quit = 'quit' # actually supposed to be a slot, but it's all mocked out anyway
|
||||
MockQThread.return_value = mocked_thread
|
||||
mocked_check_media_worker = MagicMock()
|
||||
mocked_check_media_worker.play = 'play'
|
||||
mocked_check_media_worker.result = True
|
||||
MockCheckMediaWorker.return_value = mocked_check_media_worker
|
||||
|
||||
# WHEN: check_media() is called with a valid media file
|
||||
result = player.check_media(valid_file)
|
||||
|
||||
# THEN: It should return True
|
||||
MockQThread.assert_called_once_with()
|
||||
MockCheckMediaWorker.assert_called_once_with(valid_file)
|
||||
mocked_check_media_worker.setVolume.assert_called_once_with(0)
|
||||
mocked_check_media_worker.moveToThread.assert_called_once_with(mocked_thread)
|
||||
mocked_check_media_worker.finished.connect.assert_called_once_with('quit')
|
||||
mocked_thread.started.connect.assert_called_once_with('play')
|
||||
mocked_thread.start.assert_called_once_with()
|
||||
self.assertEqual(2, mocked_thread.isRunning.call_count)
|
||||
mocked_application.processEvents.assert_called_once_with()
|
||||
self.assertTrue(result)
|
||||
|
||||
|
||||
class TestCheckMediaWorker(TestCase):
|
||||
"""
|
||||
Test the CheckMediaWorker class
|
||||
"""
|
||||
def test_constructor(self):
|
||||
"""
|
||||
Test the constructor of the CheckMediaWorker class
|
||||
"""
|
||||
# GIVEN: A file path
|
||||
path = 'file.ogv'
|
||||
|
||||
# WHEN: The CheckMediaWorker object is instantiated
|
||||
worker = CheckMediaWorker(path)
|
||||
|
||||
# THEN: The correct values should be set up
|
||||
self.assertIsNotNone(worker)
|
||||
|
||||
def test_signals_media(self):
|
||||
"""
|
||||
Test the signals() signal of the CheckMediaWorker class with a "media" origin
|
||||
"""
|
||||
# GIVEN: A CheckMediaWorker instance
|
||||
worker = CheckMediaWorker('file.ogv')
|
||||
|
||||
# WHEN: signals() is called with media and BufferedMedia
|
||||
with patch.object(worker, 'stop') as mocked_stop, \
|
||||
patch.object(worker, 'finished') as mocked_finished:
|
||||
worker.signals('media', worker.BufferedMedia)
|
||||
|
||||
# THEN: The worker should exit and the result should be True
|
||||
mocked_stop.assert_called_once_with()
|
||||
mocked_finished.emit.assert_called_once_with()
|
||||
self.assertTrue(worker.result)
|
||||
|
||||
def test_signals_error(self):
|
||||
"""
|
||||
Test the signals() signal of the CheckMediaWorker class with a "error" origin
|
||||
"""
|
||||
# GIVEN: A CheckMediaWorker instance
|
||||
worker = CheckMediaWorker('file.ogv')
|
||||
|
||||
# WHEN: signals() is called with error and BufferedMedia
|
||||
with patch.object(worker, 'stop') as mocked_stop, \
|
||||
patch.object(worker, 'finished') as mocked_finished:
|
||||
worker.signals('error', None)
|
||||
|
||||
# THEN: The worker should exit and the result should be True
|
||||
mocked_stop.assert_called_once_with()
|
||||
mocked_finished.emit.assert_called_once_with()
|
||||
self.assertFalse(worker.result)
|
212
tests/functional/openlp_plugins/bibles/test_bibleimport.py
Normal file
212
tests/functional/openlp_plugins/bibles/test_bibleimport.py
Normal file
@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2015 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
This module contains tests for the bibleimport module.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from lxml import etree, objectify
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.common.languages import Language
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestBibleImport(TestCase):
|
||||
"""
|
||||
Test the functions in the :mod:`bibleimport` module.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
test_file = BytesIO(b'<?xml version="1.0" encoding="UTF-8" ?>\n'
|
||||
b'<root>\n'
|
||||
b' <data><div>Test<p>data</p><a>to</a>keep</div></data>\n'
|
||||
b' <data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data>\n'
|
||||
b'</root>')
|
||||
self.file_patcher = patch('builtins.open', return_value=test_file)
|
||||
self.log_patcher = patch('openlp.plugins.bibles.lib.bibleimport.log')
|
||||
self.setup_patcher = patch('openlp.plugins.bibles.lib.db.BibleDB._setup')
|
||||
|
||||
self.addCleanup(self.file_patcher.stop)
|
||||
self.addCleanup(self.log_patcher.stop)
|
||||
self.addCleanup(self.setup_patcher.stop)
|
||||
|
||||
self.file_patcher.start()
|
||||
self.mock_log = self.log_patcher.start()
|
||||
self.setup_patcher.start()
|
||||
|
||||
def get_language_id_language_found_test(self):
|
||||
"""
|
||||
Test get_language_id() when called with a name found in the languages list
|
||||
"""
|
||||
# GIVEN: A mocked languages.get_language which returns language and an instance of BibleImport
|
||||
with patch('openlp.core.common.languages.get_language', return_value=Language(30, 'English', 'en')) \
|
||||
as mocked_languages_get_language, \
|
||||
patch('openlp.plugins.bibles.lib.db.BibleDB.get_language') as mocked_db_get_language:
|
||||
instance = BibleImport(MagicMock())
|
||||
instance.save_meta = MagicMock()
|
||||
|
||||
# WHEN: Calling get_language_id() with a language name and bible name
|
||||
result = instance.get_language_id('English', 'KJV')
|
||||
|
||||
# THEN: The id of the language returned from languages.get_language should be returned
|
||||
mocked_languages_get_language.assert_called_once_with('English')
|
||||
self.assertFalse(mocked_db_get_language.called)
|
||||
instance.save_meta.assert_called_once_with('language_id', 30)
|
||||
self.assertEqual(result, 30)
|
||||
|
||||
def get_language_id_language_not_found_test(self):
|
||||
"""
|
||||
Test get_language_id() when called with a name not found in the languages list
|
||||
"""
|
||||
# GIVEN: A mocked languages.get_language which returns language and an instance of BibleImport
|
||||
with patch('openlp.core.common.languages.get_language', return_value=None) \
|
||||
as mocked_languages_get_language, \
|
||||
patch('openlp.plugins.bibles.lib.db.BibleDB.get_language', return_value=20) as mocked_db_get_language:
|
||||
instance = BibleImport(MagicMock())
|
||||
instance.save_meta = MagicMock()
|
||||
|
||||
# WHEN: Calling get_language_id() with a language name and bible name
|
||||
result = instance.get_language_id('RUS', 'KJV')
|
||||
|
||||
# THEN: The id of the language returned from languages.get_language should be returned
|
||||
mocked_languages_get_language.assert_called_once_with('RUS')
|
||||
mocked_db_get_language.assert_called_once_with('KJV')
|
||||
instance.save_meta.assert_called_once_with('language_id', 20)
|
||||
self.assertEqual(result, 20)
|
||||
|
||||
def get_language_id_user_choice_test(self):
|
||||
"""
|
||||
Test get_language_id() when the language is not found and the user is asked for the language
|
||||
"""
|
||||
# GIVEN: A mocked languages.get_language which returns None a mocked BibleDB.get_language which returns a
|
||||
# language id.
|
||||
with patch('openlp.core.common.languages.get_language', return_value=None) as mocked_languages_get_language, \
|
||||
patch('openlp.plugins.bibles.lib.db.BibleDB.get_language', return_value=40) as mocked_db_get_language:
|
||||
self.mock_log.error.reset_mock()
|
||||
instance = BibleImport(MagicMock())
|
||||
instance.save_meta = MagicMock()
|
||||
|
||||
# WHEN: Calling get_language_id() with a language name and bible name
|
||||
result = instance.get_language_id('English', 'KJV')
|
||||
|
||||
# THEN: The id of the language returned from BibleDB.get_language should be returned
|
||||
mocked_languages_get_language.assert_called_once_with('English')
|
||||
mocked_db_get_language.assert_called_once_with('KJV')
|
||||
self.assertFalse(self.mock_log.error.called)
|
||||
instance.save_meta.assert_called_once_with('language_id', 40)
|
||||
self.assertEqual(result, 40)
|
||||
|
||||
def get_language_id_user_choice_rejected_test(self):
|
||||
"""
|
||||
Test get_language_id() when the language is not found and the user rejects the dilaog box
|
||||
"""
|
||||
# GIVEN: A mocked languages.get_language which returns None a mocked BibleDB.get_language which returns a
|
||||
# language id.
|
||||
with patch('openlp.core.common.languages.get_language', return_value=None) as mocked_languages_get_language, \
|
||||
patch('openlp.plugins.bibles.lib.db.BibleDB.get_language', return_value=None) as mocked_db_get_language:
|
||||
self.mock_log.error.reset_mock()
|
||||
instance = BibleImport(MagicMock())
|
||||
instance.save_meta = MagicMock()
|
||||
|
||||
# WHEN: Calling get_language_id() with a language name and bible name
|
||||
result = instance.get_language_id('Qwerty', 'KJV')
|
||||
|
||||
# THEN: None should be returned and an error should be logged
|
||||
mocked_languages_get_language.assert_called_once_with('Qwerty')
|
||||
mocked_db_get_language.assert_called_once_with('KJV')
|
||||
self.mock_log.error.assert_called_once_with('Language detection failed when importing from "KJV". '
|
||||
'User aborted language selection.')
|
||||
self.assertFalse(instance.save_meta.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def parse_xml_etree_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when called with the use_objectify default value
|
||||
"""
|
||||
# GIVEN: A sample "file" to parse
|
||||
# WHEN: Calling parse_xml
|
||||
result = BibleImport.parse_xml('file.tst')
|
||||
|
||||
# THEN: The result returned should contain the correct data, and should be an instance of eetree_Element
|
||||
self.assertEqual(etree.tostring(result),
|
||||
b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n'
|
||||
b' <data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data>\n</root>')
|
||||
self.assertIsInstance(result, etree._Element)
|
||||
|
||||
def parse_xml_etree_use_objectify_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when called with use_objectify set to True
|
||||
"""
|
||||
# GIVEN: A sample "file" to parse
|
||||
# WHEN: Calling parse_xml
|
||||
result = BibleImport.parse_xml('file.tst', use_objectify=True)
|
||||
|
||||
# THEN: The result returned should contain the correct data, and should be an instance of ObjectifiedElement
|
||||
self.assertEqual(etree.tostring(result),
|
||||
b'<root><data><div>Test<p>data</p><a>to</a>keep</div></data>'
|
||||
b'<data><unsupported>Test<x>data</x><y>to</y>discard</unsupported></data></root>')
|
||||
self.assertIsInstance(result, objectify.ObjectifiedElement)
|
||||
|
||||
def parse_xml_elements_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when given a tuple of elements to remove
|
||||
"""
|
||||
# GIVEN: A tuple of elements to remove
|
||||
elements = ('unsupported', 'x', 'y')
|
||||
|
||||
# WHEN: Calling parse_xml, with a test file
|
||||
result = BibleImport.parse_xml('file.tst', elements=elements)
|
||||
|
||||
# THEN: The result returned should contain the correct data
|
||||
self.assertEqual(etree.tostring(result),
|
||||
b'<root>\n <data><div>Test<p>data</p><a>to</a>keep</div></data>\n <data/>\n</root>')
|
||||
|
||||
def parse_xml_tags_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when given a tuple of tags to remove
|
||||
"""
|
||||
# GIVEN: A tuple of tags to remove
|
||||
tags = ('div', 'p', 'a')
|
||||
|
||||
# WHEN: Calling parse_xml, with a test file
|
||||
result = BibleImport.parse_xml('file.tst', tags=tags)
|
||||
|
||||
# THEN: The result returned should contain the correct data
|
||||
self.assertEqual(etree.tostring(result), b'<root>\n <data>Testdatatokeep</data>\n <data><unsupported>Test'
|
||||
b'<x>data</x><y>to</y>discard</unsupported></data>\n</root>')
|
||||
|
||||
def parse_xml_elements_tags_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when given a tuple of elements and of tags to remove
|
||||
"""
|
||||
# GIVEN: A tuple of elements and of tags to remove
|
||||
elements = ('unsupported', 'x', 'y')
|
||||
tags = ('div', 'p', 'a')
|
||||
|
||||
# WHEN: Calling parse_xml, with a test file
|
||||
result = BibleImport.parse_xml('file.tst', elements=elements, tags=tags)
|
||||
|
||||
# THEN: The result returned should contain the correct data
|
||||
self.assertEqual(etree.tostring(result), b'<root>\n <data>Testdatatokeep</data>\n <data/>\n</root>')
|
@ -23,13 +23,17 @@
|
||||
This module contains tests for the CSV Bible importer.
|
||||
"""
|
||||
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
from collections import namedtuple
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.plugins.bibles.lib.csvbible import CSVBible
|
||||
from openlp.plugins.bibles.lib.db import BibleDB
|
||||
from tests.functional import ANY, MagicMock, PropertyMock, call, patch
|
||||
from openlp.core.lib.exceptions import ValidationError
|
||||
from openlp.plugins.bibles.lib.bibleimport import BibleImport
|
||||
from openlp.plugins.bibles.lib.importers.csvbible import Book, CSVBible, Verse
|
||||
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..', '..', 'resources', 'bibles'))
|
||||
@ -41,14 +45,12 @@ class TestCSVImport(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher.start()
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.addCleanup(self.manager_patcher.stop)
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.manager_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.registry_patcher.stop()
|
||||
self.manager_patcher.stop()
|
||||
self.registry_patcher.start()
|
||||
|
||||
def test_create_importer(self):
|
||||
"""
|
||||
@ -58,12 +60,331 @@ class TestCSVImport(TestCase):
|
||||
mocked_manager = MagicMock()
|
||||
|
||||
# WHEN: An importer object is created
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='.', versefile='.')
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
|
||||
# THEN: The importer should be an instance of BibleDB
|
||||
self.assertIsInstance(importer, BibleDB)
|
||||
# THEN: The importer should be an instance of BibleImport
|
||||
self.assertIsInstance(importer, BibleImport)
|
||||
self.assertEqual(importer.books_file, 'books.csv')
|
||||
self.assertEqual(importer.verses_file, 'verse.csv')
|
||||
|
||||
def test_file_import(self):
|
||||
def book_namedtuple_test(self):
|
||||
"""
|
||||
Test that the Book namedtuple is created as expected
|
||||
"""
|
||||
# GIVEN: The Book namedtuple
|
||||
# WHEN: Creating an instance of Book
|
||||
result = Book('id', 'testament_id', 'name', 'abbreviation')
|
||||
|
||||
# THEN: The attributes should match up with the data we used
|
||||
self.assertEqual(result.id, 'id')
|
||||
self.assertEqual(result.testament_id, 'testament_id')
|
||||
self.assertEqual(result.name, 'name')
|
||||
self.assertEqual(result.abbreviation, 'abbreviation')
|
||||
|
||||
def verse_namedtuple_test(self):
|
||||
"""
|
||||
Test that the Verse namedtuple is created as expected
|
||||
"""
|
||||
# GIVEN: The Verse namedtuple
|
||||
# WHEN: Creating an instance of Verse
|
||||
result = Verse('book_id_name', 'chapter_number', 'number', 'text')
|
||||
|
||||
# THEN: The attributes should match up with the data we used
|
||||
self.assertEqual(result.book_id_name, 'book_id_name')
|
||||
self.assertEqual(result.chapter_number, 'chapter_number')
|
||||
self.assertEqual(result.number, 'number')
|
||||
self.assertEqual(result.text, 'text')
|
||||
|
||||
def get_book_name_id_test(self):
|
||||
"""
|
||||
Test that get_book_name() returns the correct book when called with an id
|
||||
"""
|
||||
# GIVEN: A dictionary of books with their id as the keys
|
||||
books = {1: 'Book 1', 2: 'Book 2', 3: 'Book 3'}
|
||||
|
||||
# WHEN: Calling get_book_name() and the name is an integer represented as a string
|
||||
test_data = [['1', 'Book 1'], ['2', 'Book 2'], ['3', 'Book 3']]
|
||||
for name, expected_result in test_data:
|
||||
actual_result = CSVBible.get_book_name(name, books)
|
||||
|
||||
# THEN: get_book_name() should return the book name associated with that id from the books dictionary
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
def get_book_name_test(self):
|
||||
"""
|
||||
Test that get_book_name() returns the name when called with a non integer value
|
||||
"""
|
||||
# GIVEN: A dictionary of books with their id as the keys
|
||||
books = {1: 'Book 1', 2: 'Book 2', 3: 'Book 3'}
|
||||
|
||||
# WHEN: Calling get_book_name() and the name is not an integer represented as a string
|
||||
test_data = [['Book 4', 'Book 4'], ['Book 5', 'Book 5'], ['Book 6', 'Book 6']]
|
||||
for name, expected_result in test_data:
|
||||
actual_result = CSVBible.get_book_name(name, books)
|
||||
|
||||
# THEN: get_book_name() should return the input
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
def parse_csv_file_test(self):
|
||||
"""
|
||||
Test the parse_csv_file() with sample data
|
||||
"""
|
||||
# GIVEN: A mocked csv.reader which returns an iterator with test data
|
||||
test_data = [['1', 'Line 1', 'Data 1'], ['2', 'Line 2', 'Data 2'], ['3', 'Line 3', 'Data 3']]
|
||||
TestTuple = namedtuple('TestTuple', 'line_no line_description line_data')
|
||||
|
||||
with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
|
||||
return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True) as mocked_open,\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader',
|
||||
return_value=iter(test_data)) as mocked_reader:
|
||||
|
||||
# WHEN: Calling the CSVBible parse_csv_file method with a file name and TestTuple
|
||||
result = CSVBible.parse_csv_file('file.csv', TestTuple)
|
||||
|
||||
# THEN: A list of TestTuple instances with the parsed data should be returned
|
||||
self.assertEqual(result, [TestTuple('1', 'Line 1', 'Data 1'), TestTuple('2', 'Line 2', 'Data 2'),
|
||||
TestTuple('3', 'Line 3', 'Data 3')])
|
||||
mocked_open.assert_called_once_with('file.csv', 'r', encoding='utf-8', newline='')
|
||||
mocked_reader.assert_called_once_with(ANY, delimiter=',', quotechar='"')
|
||||
|
||||
def parse_csv_file_oserror_test(self):
|
||||
"""
|
||||
Test the parse_csv_file() handles an OSError correctly
|
||||
"""
|
||||
# GIVEN: Mocked a mocked open object which raises an OSError
|
||||
with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
|
||||
return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.open', side_effect=OSError, create=True):
|
||||
|
||||
# WHEN: Calling CSVBible.parse_csv_file
|
||||
# THEN: A ValidationError should be raised
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
CSVBible.parse_csv_file('file.csv', None)
|
||||
self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed')
|
||||
|
||||
def parse_csv_file_csverror_test(self):
|
||||
"""
|
||||
Test the parse_csv_file() handles an csv.Error correctly
|
||||
"""
|
||||
# GIVEN: Mocked a csv.reader which raises an csv.Error
|
||||
with patch('openlp.plugins.bibles.lib.importers.csvbible.get_file_encoding',
|
||||
return_value={'encoding': 'utf-8', 'confidence': 0.99}),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.open', create=True),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.csv.reader', side_effect=csv.Error):
|
||||
|
||||
# WHEN: Calling CSVBible.parse_csv_file
|
||||
# THEN: A ValidationError should be raised
|
||||
with self.assertRaises(ValidationError) as context:
|
||||
CSVBible.parse_csv_file('file.csv', None)
|
||||
self.assertEqual(context.exception.msg, 'Parsing "file.csv" failed')
|
||||
|
||||
def process_books_stopped_import_test(self):
|
||||
"""
|
||||
Test process books when the import is stopped
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with the stop_import_flag set to True
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'):
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
type(importer).application = PropertyMock()
|
||||
importer.stop_import_flag = True
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling process_books
|
||||
result = importer.process_books(['Book 1'])
|
||||
|
||||
# THEN: increment_progress_bar should not be called and the return value should be None
|
||||
self.assertFalse(importer.wizard.increment_progress_bar.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def process_books_test(self):
|
||||
"""
|
||||
Test process books when it completes successfully
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with the stop_import_flag set to False, and some sample data
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.translate'):
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
type(importer).application = PropertyMock()
|
||||
importer.find_and_create_book = MagicMock()
|
||||
importer.language_id = 10
|
||||
importer.stop_import_flag = False
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
books = [Book('1', '1', '1. Mosebog', '1Mos'), Book('2', '1', '2. Mosebog', '2Mos')]
|
||||
|
||||
# WHEN: Calling process_books
|
||||
result = importer.process_books(books)
|
||||
|
||||
# THEN: translate and find_and_create_book should have been called with both book names.
|
||||
# The returned data should be a dictionary with both song's id and names.
|
||||
self.assertEqual(importer.find_and_create_book.mock_calls,
|
||||
[call('1. Mosebog', 2, 10), call('2. Mosebog', 2, 10)])
|
||||
importer.application.process_events.assert_called_once_with()
|
||||
self.assertDictEqual(result, {1: '1. Mosebog', 2: '2. Mosebog'})
|
||||
|
||||
def process_verses_stopped_import_test(self):
|
||||
"""
|
||||
Test process_verses when the import is stopped
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with the stop_import_flag set to True
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'):
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
type(importer).application = PropertyMock()
|
||||
importer.get_book_name = MagicMock()
|
||||
importer.session = MagicMock()
|
||||
importer.stop_import_flag = True
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling process_verses
|
||||
result = importer.process_verses([], [])
|
||||
|
||||
# THEN: get_book_name should not be called and the return value should be None
|
||||
self.assertFalse(importer.get_book_name.called)
|
||||
importer.wizard.increment_progress_bar.assert_called_once_with('Importing verses... done.')
|
||||
importer.application.process_events.assert_called_once_with()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def process_verses_successful_test(self):
|
||||
"""
|
||||
Test process_verses when the import is successful
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with the application and wizard attributes mocked out, and some test data.
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.translate'):
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
type(importer).application = PropertyMock()
|
||||
importer.create_verse = MagicMock()
|
||||
importer.get_book = MagicMock(return_value=Book('1', '1', '1. Mosebog', '1Mos'))
|
||||
importer.get_book_name = MagicMock(return_value='1. Mosebog')
|
||||
importer.session = MagicMock()
|
||||
importer.stop_import_flag = False
|
||||
importer.wizard = MagicMock()
|
||||
verses = [Verse(1, 1, 1, 'I Begyndelsen skabte Gud Himmelen og Jorden.'),
|
||||
Verse(1, 1, 2, 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. '
|
||||
'Men Guds Ånd svævede over Vandene.')]
|
||||
books = {1: '1. Mosebog'}
|
||||
|
||||
# WHEN: Calling process_verses
|
||||
importer.process_verses(verses, books)
|
||||
|
||||
# THEN: create_verse is called with the test data
|
||||
self.assertEqual(importer.get_book_name.mock_calls, [call(1, books), call(1, books)])
|
||||
importer.get_book.assert_called_once_with('1. Mosebog')
|
||||
self.assertEqual(importer.session.commit.call_count, 2)
|
||||
self.assertEqual(importer.create_verse.mock_calls,
|
||||
[call('1', 1, 1, 'I Begyndelsen skabte Gud Himmelen og Jorden.'),
|
||||
call('1', 1, 2, 'Og Jorden var øde og tom, og der var Mørke over Verdensdybet. '
|
||||
'Men Guds Ånd svævede over Vandene.')])
|
||||
importer.application.process_events.assert_called_once_with()
|
||||
|
||||
def do_import_invalid_language_id_test(self):
|
||||
"""
|
||||
Test do_import when the user cancels the language selection dialog box
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible and a mocked get_language which simulates the user cancelling the language box
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.log') as mocked_log:
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
importer.get_language = MagicMock(return_value=None)
|
||||
|
||||
# WHEN: Calling do_import
|
||||
result = importer.do_import('Bible Name')
|
||||
|
||||
# THEN: The log.exception method should have been called to show that it reached the except clause.
|
||||
# False should be returned.
|
||||
importer.get_language.assert_called_once_with('Bible Name')
|
||||
mocked_log.exception.assert_called_once_with('Could not import CSV bible')
|
||||
self.assertFalse(result)
|
||||
|
||||
def do_import_stop_import_test(self):
|
||||
"""
|
||||
Test do_import when the import is stopped
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with stop_import set to True
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.log') as mocked_log:
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verse.csv')
|
||||
importer.get_language = MagicMock(return_value=10)
|
||||
importer.parse_csv_file = MagicMock(return_value=['Book 1', 'Book 2', 'Book 3'])
|
||||
importer.process_books = MagicMock()
|
||||
importer.stop_import_flag = True
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling do_import
|
||||
result = importer.do_import('Bible Name')
|
||||
|
||||
# THEN: log.exception should not be called, parse_csv_file should only be called once,
|
||||
# and False should be returned.
|
||||
self.assertFalse(mocked_log.exception.called)
|
||||
importer.parse_csv_file.assert_called_once_with('books.csv', Book)
|
||||
importer.process_books.assert_called_once_with(['Book 1', 'Book 2', 'Book 3'])
|
||||
self.assertFalse(result)
|
||||
|
||||
def do_import_stop_import_2_test(self):
|
||||
"""
|
||||
Test do_import when the import is stopped
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible with stop_import which is True the second time of calling
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.log') as mocked_log:
|
||||
CSVBible.stop_import_flag = PropertyMock(side_effect=[False, True])
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verses.csv')
|
||||
importer.get_language = MagicMock(return_value=10)
|
||||
importer.parse_csv_file = MagicMock(side_effect=[['Book 1'], ['Verse 1']])
|
||||
importer.process_books = MagicMock(return_value=['Book 1'])
|
||||
importer.process_verses = MagicMock(return_value=['Verse 1'])
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling do_import
|
||||
result = importer.do_import('Bible Name')
|
||||
|
||||
# THEN: log.exception should not be called, parse_csv_file should be called twice,
|
||||
# and False should be returned.
|
||||
self.assertFalse(mocked_log.exception.called)
|
||||
self.assertEqual(importer.parse_csv_file.mock_calls, [call('books.csv', Book), call('verses.csv', Verse)])
|
||||
importer.process_verses.assert_called_once_with(['Verse 1'], ['Book 1'])
|
||||
self.assertFalse(result)
|
||||
|
||||
# Cleanup
|
||||
del CSVBible.stop_import_flag
|
||||
|
||||
def do_import_success_test(self):
|
||||
"""
|
||||
Test do_import when the import succeeds
|
||||
"""
|
||||
# GIVEN: An instance of CSVBible
|
||||
mocked_manager = MagicMock()
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.lib.importers.csvbible.log') as mocked_log:
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile='books.csv', versefile='verses.csv')
|
||||
importer.get_language = MagicMock(return_value=10)
|
||||
importer.parse_csv_file = MagicMock(side_effect=[['Book 1'], ['Verse 1']])
|
||||
importer.process_books = MagicMock(return_value=['Book 1'])
|
||||
importer.process_verses = MagicMock(return_value=['Verse 1'])
|
||||
importer.session = MagicMock()
|
||||
importer.stop_import_flag = False
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling do_import
|
||||
result = importer.do_import('Bible Name')
|
||||
|
||||
# THEN: log.exception should not be called, parse_csv_file should be called twice,
|
||||
# and True should be returned.
|
||||
self.assertFalse(mocked_log.exception.called)
|
||||
self.assertEqual(importer.parse_csv_file.mock_calls, [call('books.csv', Book), call('verses.csv', Verse)])
|
||||
importer.process_books.assert_called_once_with(['Book 1'])
|
||||
importer.process_verses.assert_called_once_with(['Verse 1'], ['Book 1'])
|
||||
self.assertTrue(result)
|
||||
|
||||
def file_import_test(self):
|
||||
"""
|
||||
Test the actual import of CSV Bible file
|
||||
"""
|
||||
@ -73,7 +394,7 @@ class TestCSVImport(TestCase):
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
books_file = os.path.join(TEST_PATH, 'dk1933-books.csv')
|
||||
verses_file = os.path.join(TEST_PATH, 'dk1933-verses.csv')
|
||||
with patch('openlp.plugins.bibles.lib.csvbible.CSVBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.csvbible.CSVBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = CSVBible(mocked_manager, path='.', name='.', booksfile=books_file, versefile=verses_file)
|
||||
|
@ -26,7 +26,7 @@ from unittest import TestCase
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from tests.functional import patch, MagicMock
|
||||
from openlp.plugins.bibles.lib.http import BSExtract
|
||||
from openlp.plugins.bibles.lib.importers.http import BSExtract
|
||||
|
||||
# TODO: Items left to test
|
||||
# BGExtract
|
||||
@ -68,11 +68,11 @@ class TestBSExtract(TestCase):
|
||||
# get_books_from_http
|
||||
# _get_application
|
||||
def setUp(self):
|
||||
self.get_soup_for_bible_ref_patcher = patch('openlp.plugins.bibles.lib.http.get_soup_for_bible_ref')
|
||||
self.log_patcher = patch('openlp.plugins.bibles.lib.http.log')
|
||||
self.send_error_message_patcher = patch('openlp.plugins.bibles.lib.http.send_error_message')
|
||||
self.socket_patcher = patch('openlp.plugins.bibles.lib.http.socket')
|
||||
self.urllib_patcher = patch('openlp.plugins.bibles.lib.http.urllib')
|
||||
self.get_soup_for_bible_ref_patcher = patch('openlp.plugins.bibles.lib.importers.http.get_soup_for_bible_ref')
|
||||
self.log_patcher = patch('openlp.plugins.bibles.lib.importers.http.log')
|
||||
self.send_error_message_patcher = patch('openlp.plugins.bibles.lib.importers.http.send_error_message')
|
||||
self.socket_patcher = patch('openlp.plugins.bibles.lib.importers.http.socket')
|
||||
self.urllib_patcher = patch('openlp.plugins.bibles.lib.importers.http.urllib')
|
||||
|
||||
self.mock_get_soup_for_bible_ref = self.get_soup_for_bible_ref_patcher.start()
|
||||
self.mock_log = self.log_patcher.start()
|
||||
|
@ -28,7 +28,7 @@ import json
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.plugins.bibles.lib.opensong import OpenSongBible
|
||||
from openlp.plugins.bibles.lib.importers.opensong import OpenSongBible
|
||||
from openlp.plugins.bibles.lib.db import BibleDB
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
@ -72,7 +72,7 @@ class TestOpenSongImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'opensong-dk1933.xml'
|
||||
with patch('openlp.plugins.bibles.lib.opensong.OpenSongBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.opensong.OpenSongBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OpenSongBible(mocked_manager, path='.', name='.', filename='')
|
||||
@ -98,7 +98,7 @@ class TestOpenSongImport(TestCase):
|
||||
Test that we give an error message if trying to import a zefania bible
|
||||
"""
|
||||
# GIVEN: A mocked out "manager" and mocked out critical_error_message_box and an import
|
||||
with patch('openlp.plugins.bibles.lib.opensong.critical_error_message_box') as \
|
||||
with patch('openlp.plugins.bibles.lib.importers.opensong.critical_error_message_box') as \
|
||||
mocked_critical_error_message_box:
|
||||
mocked_manager = MagicMock()
|
||||
importer = OpenSongBible(mocked_manager, path='.', name='.', filename='')
|
||||
|
@ -28,7 +28,7 @@ import json
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.plugins.bibles.lib.osis import OSISBible
|
||||
from openlp.plugins.bibles.lib.importers.osis import OSISBible
|
||||
from openlp.plugins.bibles.lib.db import BibleDB
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
@ -72,7 +72,7 @@ class TestOsisImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'osis-dk1933.xml'
|
||||
with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OSISBible(mocked_manager, path='.', name='.', filename='')
|
||||
@ -102,7 +102,7 @@ class TestOsisImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'kjv.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'osis-kjv.xml'
|
||||
with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OSISBible(mocked_manager, path='.', name='.', filename='')
|
||||
@ -132,7 +132,7 @@ class TestOsisImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'web.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'osis-web.xml'
|
||||
with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OSISBible(mocked_manager, path='.', name='.', filename='')
|
||||
@ -162,7 +162,7 @@ class TestOsisImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'osis-dk1933-empty-verse.xml'
|
||||
with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.osis.OSISBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = OSISBible(mocked_manager, path='.', name='.', filename='')
|
||||
|
@ -29,7 +29,7 @@ from unittest import TestCase, skipUnless
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
try:
|
||||
from openlp.plugins.bibles.lib.sword import SwordBible
|
||||
from openlp.plugins.bibles.lib.importers.sword import SwordBible
|
||||
HAS_PYSWORD = True
|
||||
except ImportError:
|
||||
HAS_PYSWORD = False
|
||||
@ -68,8 +68,8 @@ class TestSwordImport(TestCase):
|
||||
# THEN: The importer should be an instance of BibleDB
|
||||
self.assertIsInstance(importer, BibleDB)
|
||||
|
||||
@patch('openlp.plugins.bibles.lib.sword.SwordBible.application')
|
||||
@patch('openlp.plugins.bibles.lib.sword.modules')
|
||||
@patch('openlp.plugins.bibles.lib.importers.sword.SwordBible.application')
|
||||
@patch('openlp.plugins.bibles.lib.importers.sword.modules')
|
||||
@patch('openlp.core.common.languages')
|
||||
def test_simple_import(self, mocked_languages, mocked_pysword_modules, mocked_application):
|
||||
"""
|
||||
|
@ -28,7 +28,7 @@ import json
|
||||
from unittest import TestCase
|
||||
|
||||
from tests.functional import MagicMock, patch
|
||||
from openlp.plugins.bibles.lib.zefania import ZefaniaBible
|
||||
from openlp.plugins.bibles.lib.importers.zefania import ZefaniaBible
|
||||
from openlp.plugins.bibles.lib.db import BibleDB
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
@ -72,7 +72,7 @@ class TestZefaniaImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'zefania-dk1933.xml'
|
||||
with patch('openlp.plugins.bibles.lib.zefania.ZefaniaBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.zefania.ZefaniaBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='')
|
||||
@ -102,7 +102,7 @@ class TestZefaniaImport(TestCase):
|
||||
result_file = open(os.path.join(TEST_PATH, 'rst.json'), 'rb')
|
||||
test_data = json.loads(result_file.read().decode())
|
||||
bible_file = 'zefania-rst.xml'
|
||||
with patch('openlp.plugins.bibles.lib.zefania.ZefaniaBible.application'):
|
||||
with patch('openlp.plugins.bibles.lib.importers.zefania.ZefaniaBible.application'):
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
importer = ZefaniaBible(mocked_manager, path='.', name='.', filename='')
|
||||
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
# pylint: disable=protected-access
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
@ -28,14 +29,13 @@ from urllib.error import URLError
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from tests.helpers.songfileimport import SongImportTestHelper
|
||||
from openlp.core import Registry
|
||||
from openlp.plugins.songs.forms.songselectform import SongSelectForm, SearchWorker
|
||||
from openlp.plugins.songs.lib import Song
|
||||
from openlp.plugins.songs.lib.songselect import SongSelectImport, LOGOUT_URL, BASE_URL
|
||||
from openlp.plugins.songs.lib.importers.cclifile import CCLIFileImport
|
||||
|
||||
from tests.functional import MagicMock, patch, call
|
||||
from tests.helpers.songfileimport import SongImportTestHelper
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
TEST_PATH = os.path.abspath(
|
||||
@ -71,7 +71,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_login_page = MagicMock()
|
||||
mocked_login_page.find.return_value = {'value': 'blah'}
|
||||
mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
|
||||
MockedBeautifulSoup.return_value = mocked_login_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
@ -112,7 +112,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
mocked_login_page = MagicMock()
|
||||
mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
|
||||
mocked_login_page.find.side_effect = [{'value': 'blah'}, MagicMock()]
|
||||
MockedBeautifulSoup.return_value = mocked_login_page
|
||||
mock_callback = MagicMock()
|
||||
importer = SongSelectImport(None)
|
||||
@ -165,7 +165,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
self.assertEqual(0, mock_callback.call_count, 'callback should not have been called')
|
||||
self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once')
|
||||
self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
mocked_results_page.find_all.assert_called_with('div', 'song-result')
|
||||
self.assertEqual([], results, 'The search method should have returned an empty list')
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.build_opener')
|
||||
@ -177,12 +177,18 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
# first search result
|
||||
mocked_result1 = MagicMock()
|
||||
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
|
||||
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
|
||||
mocked_result1.find.side_effect = [
|
||||
MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
|
||||
MagicMock(string='James, John'),
|
||||
MagicMock(find=MagicMock(return_value={'href': '/url1'}))
|
||||
]
|
||||
# second search result
|
||||
mocked_result2 = MagicMock()
|
||||
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
|
||||
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
|
||||
mocked_result2.find.side_effect = [
|
||||
MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
|
||||
MagicMock(string='Philip'),
|
||||
MagicMock(find=MagicMock(return_value={'href': '/url2'}))
|
||||
]
|
||||
# rest of the stuff
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
@ -199,10 +205,10 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
|
||||
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
|
||||
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
mocked_results_page.find_all.assert_called_with('div', 'song-result')
|
||||
expected_list = [
|
||||
{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}
|
||||
{'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}
|
||||
]
|
||||
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
|
||||
|
||||
@ -215,16 +221,25 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
# GIVEN: A bunch of mocked out stuff and an importer object
|
||||
# first search result
|
||||
mocked_result1 = MagicMock()
|
||||
mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}]
|
||||
mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')]
|
||||
mocked_result1.find.side_effect = [
|
||||
MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
|
||||
MagicMock(string='James, John'),
|
||||
MagicMock(find=MagicMock(return_value={'href': '/url1'}))
|
||||
]
|
||||
# second search result
|
||||
mocked_result2 = MagicMock()
|
||||
mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}]
|
||||
mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')]
|
||||
mocked_result2.find.side_effect = [
|
||||
MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
|
||||
MagicMock(string='Philip'),
|
||||
MagicMock(find=MagicMock(return_value={'href': '/url2'}))
|
||||
]
|
||||
# third search result
|
||||
mocked_result3 = MagicMock()
|
||||
mocked_result3.find.side_effect = [MagicMock(string='Title 3'), {'href': '/url3'}]
|
||||
mocked_result3.find_all.return_value = [MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2')]
|
||||
mocked_result3.find.side_effect = [
|
||||
MagicMock(find=MagicMock(return_value=MagicMock(string='Title 3'))),
|
||||
MagicMock(string='Luke, Matthew'),
|
||||
MagicMock(find=MagicMock(return_value={'href': '/url3'}))
|
||||
]
|
||||
# rest of the stuff
|
||||
mocked_opener = MagicMock()
|
||||
mocked_build_opener.return_value = mocked_opener
|
||||
@ -241,9 +256,9 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
|
||||
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
|
||||
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
|
||||
mocked_results_page.find_all.assert_called_with('li', 'result pane')
|
||||
expected_list = [{'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
|
||||
mocked_results_page.find_all.assert_called_with('div', 'song-result')
|
||||
expected_list = [{'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
|
||||
{'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}]
|
||||
self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.build_opener')
|
||||
@ -337,7 +352,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary')
|
||||
self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice')
|
||||
self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice')
|
||||
self.assertEqual([call('section', 'lyrics'), call('section', 'lyrics')],
|
||||
self.assertEqual([call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')],
|
||||
mocked_lyrics_page.find.call_args_list,
|
||||
'The find() method should have been called with the right arguments')
|
||||
self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list,
|
||||
@ -348,8 +363,9 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')
|
||||
|
||||
@patch('openlp.plugins.songs.lib.songselect.clean_song')
|
||||
@patch('openlp.plugins.songs.lib.songselect.Topic')
|
||||
@patch('openlp.plugins.songs.lib.songselect.Author')
|
||||
def test_save_song_new_author(self, MockedAuthor, mocked_clean_song):
|
||||
def test_save_song_new_author(self, MockedAuthor, MockedTopic, mocked_clean_song):
|
||||
"""
|
||||
Test that saving a song with a new author performs the correct actions
|
||||
"""
|
||||
@ -366,6 +382,7 @@ class TestSongSelectImport(TestCase, TestMixin):
|
||||
'ccli_number': '123456'
|
||||
}
|
||||
MockedAuthor.display_name.__eq__.return_value = False
|
||||
MockedTopic.name.__eq__.return_value = False
|
||||
mocked_db_manager = MagicMock()
|
||||
mocked_db_manager.get_object_filtered.return_value = None
|
||||
importer = SongSelectImport(mocked_db_manager)
|
||||
@ -848,7 +865,7 @@ class TestSearchWorker(TestCase, TestMixin):
|
||||
|
||||
# WHEN: The start() method is called
|
||||
with patch.object(worker, 'found_song') as mocked_found_song:
|
||||
worker._found_song_callback(song)
|
||||
worker._found_song_callback(song) # pylint: disable=protected-access
|
||||
|
||||
# THEN: The "found_song" signal should have been emitted
|
||||
mocked_found_song.emit.assert_called_with(song)
|
||||
|
@ -26,7 +26,7 @@ class and methods.
|
||||
import os
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib.projector.db import Projector, ProjectorDB
|
||||
from openlp.core.ui import ProjectorEditForm
|
||||
|
||||
@ -63,43 +63,41 @@ class TestProjectorEditForm(TestCase, TestMixin):
|
||||
:return: None
|
||||
"""
|
||||
self.projectordb.session.close()
|
||||
del(self.projector_form)
|
||||
del self.projector_form
|
||||
self.destroy_settings()
|
||||
|
||||
def test_edit_form_add_projector(self):
|
||||
@patch('openlp.core.ui.projector.editform.QtWidgets.QDialog.exec')
|
||||
def test_edit_form_add_projector(self, mocked_exec):
|
||||
"""
|
||||
Test projector edit form with no parameters creates a new entry.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
# GIVEN: Mocked setup
|
||||
with patch('openlp.core.ui.projector.editform.QDialog.exec'):
|
||||
# WHEN: Calling edit form with no parameters
|
||||
self.projector_form.exec()
|
||||
item = self.projector_form.projector
|
||||
|
||||
# WHEN: Calling edit form with no parameters
|
||||
self.projector_form.exec()
|
||||
item = self.projector_form.projector
|
||||
# THEN: Should be creating a new instance
|
||||
self.assertTrue(self.projector_form.new_projector,
|
||||
'Projector edit form should be marked as a new entry')
|
||||
self.assertTrue((item.ip is None and item.name is None),
|
||||
'Projector edit form should have a new Projector() instance to edit')
|
||||
|
||||
# THEN: Should be creating a new instance
|
||||
self.assertTrue(self.projector_form.new_projector,
|
||||
'Projector edit form should be marked as a new entry')
|
||||
self.assertTrue((item.ip is None and item.name is None),
|
||||
'Projector edit form should have a new Projector() instance to edit')
|
||||
|
||||
def test_edit_form_edit_projector(self):
|
||||
@patch('openlp.core.ui.projector.editform.QtWidgets.QDialog.exec')
|
||||
def test_edit_form_edit_projector(self, mocked_exec):
|
||||
"""
|
||||
Test projector edit form with existing projector entry
|
||||
|
||||
:return:
|
||||
"""
|
||||
# GIVEN: Mocked setup
|
||||
with patch('openlp.core.ui.projector.editform.QDialog.exec'):
|
||||
# WHEN: Calling edit form with existing projector instance
|
||||
self.projector_form.exec(projector=Projector(**TEST1_DATA))
|
||||
item = self.projector_form.projector
|
||||
|
||||
# WHEN: Calling edit form with existing projector instance
|
||||
self.projector_form.exec(projector=Projector(**TEST1_DATA))
|
||||
item = self.projector_form.projector
|
||||
|
||||
# THEN: Should be editing an existing entry
|
||||
self.assertFalse(self.projector_form.new_projector,
|
||||
'Projector edit form should be marked as existing entry')
|
||||
self.assertTrue((item.ip is TEST1_DATA['ip'] and item.name is TEST1_DATA['name']),
|
||||
'Projector edit form should have TEST1_DATA() instance to edit')
|
||||
# THEN: Should be editing an existing entry
|
||||
self.assertFalse(self.projector_form.new_projector,
|
||||
'Projector edit form should be marked as existing entry')
|
||||
self.assertTrue((item.ip is TEST1_DATA['ip'] and item.name is TEST1_DATA['name']),
|
||||
'Projector edit form should have TEST1_DATA() instance to edit')
|
||||
|
@ -25,7 +25,7 @@
|
||||
from unittest import TestCase, skip
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.plugins.bibles.lib.http import BGExtract, CWExtract, BSExtract
|
||||
from openlp.plugins.bibles.lib.importers.http import BGExtract, CWExtract, BSExtract
|
||||
from tests.interfaces import MagicMock
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user