diff --git a/openlp/core/app.py b/openlp/core/app.py index 45bafb0c6..bc030f61c 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -292,7 +292,7 @@ def parse_options(args=None): :return: a tuple of parsed options of type optparse.Value and a list of remaining argsZ """ # Set up command line options. - parser = argparse.ArgumentParser(prog='openlp.py') + parser = argparse.ArgumentParser(prog='openlp') parser.add_argument('-e', '--no-error-form', dest='no_error_form', action='store_true', help='Disable the error notification form.') parser.add_argument('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL', diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 0148bce99..a7215e394 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -60,7 +60,6 @@ def get_local_ip4(): :returns: Dict of interfaces """ - # Get the local IPv4 active address(es) that are NOT localhost (lo or '127.0.0.1') log.debug('Getting local IPv4 interface(es) information') my_ip4 = {} for iface in QNetworkInterface.allInterfaces(): @@ -70,8 +69,6 @@ def get_local_ip4(): log.debug('Checking address(es) protocol') for address in iface.addressEntries(): ip = address.ip() - # NOTE: Next line will skip if interface is localhost - keep for now until we decide about it later - # if (ip.protocol() == QAbstractSocket.IPv4Protocol) and (ip != QHostAddress.LocalHost): log.debug('Checking for protocol == IPv4Protocol') if ip.protocol() == QAbstractSocket.IPv4Protocol: log.debug('Getting interface information') @@ -83,12 +80,13 @@ def get_local_ip4(): ip.toIPv4Address()).toString() } log.debug('Adding {iface} to active list'.format(iface=iface.name())) + if len(my_ip4) == 0: + log.warning('No active IPv4 network interfaces detected') + return my_ip4 if 'localhost' in my_ip4: log.debug('Renaming windows localhost to lo') my_ip4['lo'] = my_ip4['localhost'] my_ip4.pop('localhost') - if len(my_ip4) == 0: - log.warning('No active IPv4 network interfaces detected') if len(my_ip4) == 1: if 'lo' in my_ip4: # No active interfaces - so leave localhost in there diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 14e6e4577..7b7c5781d 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -26,18 +26,13 @@ import logging import os import sys +import appdirs + import openlp from openlp.core.common import get_frozen_path, is_win, is_macosx from openlp.core.common.path import Path, create_paths from openlp.core.common.settings import Settings -if not is_win() and not is_macosx(): - try: - from xdg import BaseDirectory - XDG_BASE_AVAILABLE = True - except ImportError: - XDG_BASE_AVAILABLE = False - log = logging.getLogger(__name__) FROZEN_APP_PATH = Path(sys.argv[0]).parent @@ -143,8 +138,10 @@ def _get_os_dir_path(dir_type): elif dir_type == AppLocation.LanguageDir: return Path(openlp.__file__).parent return openlp_folder_path - elif is_macosx(): - openlp_folder_path = Path(os.getenv('HOME'), 'Library', 'Application Support', 'openlp') + + dirs = appdirs.AppDirs('openlp', multipath=True) + if is_macosx(): + openlp_folder_path = Path(dirs.user_data_dir) if dir_type == AppLocation.DataDir: return openlp_folder_path / 'Data' elif dir_type == AppLocation.LanguageDir: @@ -152,15 +149,15 @@ def _get_os_dir_path(dir_type): return openlp_folder_path else: if dir_type == AppLocation.LanguageDir: - directory = Path('/usr', 'local', 'share', 'openlp') + site_dirs = dirs.site_data_dir.split(os.pathsep) + directory = Path(site_dirs[0]) if directory.exists(): return directory - return Path('/usr', 'share', 'openlp') - if XDG_BASE_AVAILABLE: - if dir_type == AppLocation.DataDir: - return Path(BaseDirectory.xdg_data_home, 'openlp') - elif dir_type == AppLocation.CacheDir: - return Path(BaseDirectory.xdg_cache_home, 'openlp') + return Path(site_dirs[1]) + if dir_type == AppLocation.DataDir: + return Path(dirs.user_data_dir) + elif dir_type == AppLocation.CacheDir: + return Path(dirs.user_cache_dir) if dir_type == AppLocation.DataDir: return Path(os.getenv('HOME'), '.openlp', 'data') return Path(os.getenv('HOME'), '.openlp') diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py index 89730b87e..39ebe8d83 100644 --- a/openlp/core/common/registry.py +++ b/openlp/core/common/registry.py @@ -57,8 +57,7 @@ class Registry(object): registry.functions_list = {} registry.working_flags = {} # Allow the tests to remove Registry entries but not the live system - registry.running_under_test = 'nose' in sys.argv[0] - registry.running_under_test = 'pytest' in sys.argv[0] + registry.running_under_test = 'nose' in sys.argv[0] or 'pytest' in sys.argv[0] registry.initialising = True return registry diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 74d686b09..403b82d97 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -217,7 +217,8 @@ class Settings(QtCore.QSettings): 'projector/last directory export': None, 'projector/poll time': 20, # PJLink timeout is 30 seconds 'projector/socket timeout': 5, # 5 second socket timeout - 'projector/source dialog type': 0 # Source select dialog box type + 'projector/source dialog type': 0, # Source select dialog box type + 'projector/udp broadcast listen': False # Enable/disable listening for PJLink 2 UDP broadcast packets } __file_path__ = '' # Settings upgrades prior to 3.0 @@ -274,7 +275,11 @@ class Settings(QtCore.QSettings): ('songuasge/db password', 'songusage/db password', []), ('songuasge/db hostname', 'songusage/db hostname', []), ('songuasge/db database', 'songusage/db database', []), - ('presentations / Powerpoint Viewer', '', []) + ('presentations / Powerpoint Viewer', '', []), + ('bibles/proxy name', '', []), # Just remove these bible proxy settings. They weren't used in 2.4! + ('bibles/proxy address', '', []), + ('bibles/proxy username', '', []), + ('bibles/proxy password', '', []) ] @staticmethod diff --git a/openlp/core/projectors/editform.py b/openlp/core/projectors/editform.py index ef88605ef..04ad668ca 100644 --- a/openlp/core/projectors/editform.py +++ b/openlp/core/projectors/editform.py @@ -179,6 +179,7 @@ class ProjectorEditForm(QtWidgets.QDialog, Ui_ProjectorEditForm): Validate input before accepting input. """ log.debug('accept_me() signal received') + valid = True if len(self.name_text.text().strip()) < 1: QtWidgets.QMessageBox.warning(self, translate('OpenLP.ProjectorEdit', 'Name Not Set'), diff --git a/openlp/core/projectors/manager.py b/openlp/core/projectors/manager.py index c2575c60a..add7cd4c0 100644 --- a/openlp/core/projectors/manager.py +++ b/openlp/core/projectors/manager.py @@ -32,13 +32,13 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common.i18n import translate from openlp.core.ui.icons import UiIcons from openlp.core.common.mixins import LogMixin, RegistryProperties -from openlp.core.common.registry import RegistryBase +from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.lib.ui import create_widget_action from openlp.core.projectors import DialogSourceStyle from openlp.core.projectors.constants import E_AUTHENTICATION, E_ERROR, E_NETWORK, E_NOT_CONNECTED, \ E_SOCKET_TIMEOUT, E_UNKNOWN_SOCKET_ERROR, S_CONNECTED, S_CONNECTING, S_COOLDOWN, S_INITIALIZE, \ - S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP, PJLINK_PORT, STATUS_CODE, STATUS_MSG, QSOCKET_STATE + S_NOT_CONNECTED, S_OFF, S_ON, S_STANDBY, S_WARMUP, STATUS_CODE, STATUS_MSG, QSOCKET_STATE from openlp.core.projectors.db import ProjectorDB from openlp.core.projectors.editform import ProjectorEditForm @@ -297,7 +297,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM self.projector_list = [] self.source_select_form = None # Dictionary of PJLinkUDP objects to listen for UDP broadcasts from PJLink 2+ projectors. - # Key is port number that projectors use + # Key is port number self.pjlink_udp = {} # Dict for matching projector status to display icon self.status_icons = { @@ -335,10 +335,6 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM """ Post-initialize setups. """ - # Default PJLink port UDP socket - log.debug('Creating PJLinkUDP listener for default port {port}'.format(port=PJLINK_PORT)) - self.pjlink_udp = {PJLINK_PORT: PJLinkUDP(port=PJLINK_PORT)} - self.pjlink_udp[PJLINK_PORT].bind(PJLINK_PORT) # Set 1.5 second delay before loading all projectors if self.autostart: log.debug('Delaying 1.5 seconds before loading all projectors') @@ -351,6 +347,36 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM self.projector_form.editProjector.connect(self.edit_projector_from_wizard) self.projector_list_widget.itemSelectionChanged.connect(self.update_icons) + def udp_listen_add(self, port): + """ + Add UDP broadcast listener + """ + if port in self.pjlink_udp: + log.warning('UDP Listener for port {port} already added - skipping'.format(port=port)) + else: + log.debug('Adding UDP listener on port {port}'.format(port=port)) + self.pjlink_udp[port] = PJLinkUDP(port=port) + Registry().execute('udp_broadcast_add', port=port, callback=self.pjlink_udp[port].check_settings) + + def udp_listen_delete(self, port): + """ + Remove a UDP broadcast listener + """ + log.debug('Checking for UDP port {port} listener deletion'.format(port=port)) + if port not in self.pjlink_udp: + log.warn('UDP listener for port {port} not there - skipping delete'.format(port=port)) + return + keep_port = False + for item in self.projector_list: + if port == item.link.port: + keep_port = True + if keep_port: + log.warn('UDP listener for port {port} needed for other projectors - skipping delete'.format(port=port)) + return + Registry().execute('udp_broadcast_remove', port=port) + del self.pjlink_udp[port] + log.debug('UDP listener for port {port} deleted'.format(port=port)) + def get_settings(self): """ Retrieve the saved settings @@ -518,25 +544,22 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM except (AttributeError, TypeError): pass try: - projector.poll_timer.stop() - projector.poll_timer.timeout.disconnect(projector.link.poll_loop) + projector.link.poll_timer.stop() + projector.link.poll_timer.timeout.disconnect(projector.link.poll_loop) except (AttributeError, TypeError): pass try: - projector.socket_timer.stop() - projector.socket_timer.timeout.disconnect(projector.link.socket_abort) - except (AttributeError, TypeError): - pass - # Disconnect signals from projector being deleted - try: - self.pjlink_udp[projector.link.port].data_received.disconnect(projector.link.get_buffer) + projector.link.socket_timer.stop() + projector.link.socket_timer.timeout.disconnect(projector.link.socket_abort) except (AttributeError, TypeError): pass + old_port = projector.link.port # Rebuild projector list new_list = [] for item in self.projector_list: if item.link.db_item.id == projector.link.db_item.id: + log.debug('Removing projector "{item}"'.format(item=item.link.name)) continue new_list.append(item) self.projector_list = new_list @@ -546,6 +569,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM log.warning('Delete projector {item} failed'.format(item=projector.db_item)) for item in self.projector_list: log.debug('New projector list - item: {ip} {name}'.format(ip=item.link.ip, name=item.link.name)) + self.udp_listen_delete(old_port) def on_disconnect_projector(self, opt=None): """ @@ -748,15 +772,8 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM item.link.projectorAuthentication.connect(self.authentication_error) item.link.projectorNoAuthentication.connect(self.no_authentication_error) item.link.projectorUpdateIcons.connect(self.update_icons) - # Connect UDP signal to projector instances with same port - if item.link.port not in self.pjlink_udp: - log.debug('Adding new PJLinkUDP listener fo port {port}'.format(port=item.link.port)) - self.pjlink_udp[item.link.port] = PJLinkUDP(port=item.link.port) - self.pjlink_udp[item.link.port].bind(item.link.port) - log.debug('Connecting PJLinkUDP port {port} signal to "{item}"'.format(port=item.link.port, - item=item.link.name)) - self.pjlink_udp[item.link.port].data_received.connect(item.link.get_buffer) - + # Add UDP listener for new projector port + self.udp_listen_add(item.link.port) self.projector_list.append(item) if start: item.link.connect_to_host() @@ -783,13 +800,25 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM :param projector: Projector() instance of projector with updated information """ log.debug('edit_projector_from_wizard(ip={ip})'.format(ip=projector.ip)) + old_port = self.old_projector.link.port + old_ip = self.old_projector.link.ip self.old_projector.link.name = projector.name self.old_projector.link.ip = projector.ip self.old_projector.link.pin = None if projector.pin == '' else projector.pin - self.old_projector.link.port = projector.port self.old_projector.link.location = projector.location self.old_projector.link.notes = projector.notes self.old_projector.widget.setText(projector.name) + self.old_projector.link.port = int(projector.port) + # Update projector list items + for item in self.projector_list: + if item.link.ip == old_ip: + item.link.port = int(projector.port) + # NOTE: This assumes (!) we are using IP addresses as keys + break + # Update UDP listeners before setting old_projector.port + if old_port != projector.port: + self.udp_listen_delete(old_port) + self.udp_listen_add(int(projector.port)) def _load_projectors(self): """' diff --git a/openlp/core/projectors/pjlink.py b/openlp/core/projectors/pjlink.py index 3ea1bb414..cd178bb48 100644 --- a/openlp/core/projectors/pjlink.py +++ b/openlp/core/projectors/pjlink.py @@ -98,32 +98,51 @@ class PJLinkUDP(QtNetwork.QUdpSocket): self.search_active = False self.search_time = 30000 # 30 seconds for allowed time self.search_timer = QtCore.QTimer() + self.udp_broadcast_listen_setting = False + log.debug('(UDP:{port}) PJLinkUDP() Initialized'.format(port=self.port)) + if Settings().value('projector/udp broadcast listen'): + self.udp_start() + + def udp_start(self): + """ + Start listening on UDP port + """ + log.debug('(UDP:{port}) Start called'.format(port=self.port)) self.readyRead.connect(self.get_datagram) - log.debug('(UDP) PJLinkUDP() Initialized for port {port}'.format(port=self.port)) + self.check_settings(checked=Settings().value('projector/udp broadcast listen')) + + def udp_stop(self): + """ + Stop listening on UDP port + """ + log.debug('(UDP:{port}) Stopping listener'.format(port=self.port)) + self.close() + self.readyRead.disconnect(self.get_datagram) @QtCore.pyqtSlot() def get_datagram(self): """ Retrieve packet and basic checks """ - log.debug('(UDP) get_datagram() - Receiving data') + log.debug('(UDP:{port}) get_datagram() - Receiving data'.format(port=self.port)) read_size = self.pendingDatagramSize() if -1 == read_size: - log.warning('(UDP) No data (-1)') + log.warning('(UDP:{port}) No data (-1)'.format(port=self.port)) return elif 0 == read_size: - log.warning('(UDP) get_datagram() called when pending data size is 0') + log.warning('(UDP:{port}) get_datagram() called when pending data size is 0'.format(port=self.port)) return elif read_size > PJLINK_MAX_PACKET: - log.warning('(UDP) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size)) + log.warning('(UDP:{port}) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size, + port=self.port)) return data_in, peer_host, peer_port = self.readDatagram(read_size) data = data_in.decode('utf-8') if isinstance(data_in, bytes) else data_in - log.debug('(UDP) {size} bytes received from {adx} on port {port}'.format(size=len(data), - adx=peer_host.toString(), - port=self.port)) - log.debug('(UDP) packet "{data}"'.format(data=data)) - log.debug('(UDP) Sending data_received signal to projectors') + log.debug('(UDP:{port}) {size} bytes received from {adx}'.format(size=len(data), + adx=peer_host.toString(), + port=self.port)) + log.debug('(UDP:{port}) packet "{data}"'.format(data=data, port=self.port)) + log.debug('(UDP:{port}) Sending data_received signal to projectors'.format(port=self.port)) self.data_received.emit(peer_host, self.localPort(), data) return @@ -143,6 +162,25 @@ class PJLinkUDP(QtNetwork.QUdpSocket): self.search_active = False self.search_timer.stop() + def check_settings(self, checked): + """ + Update UDP listening state based on settings change. + NOTE: This method is called by projector settings tab and setup/removed by ProjectorManager + """ + if self.udp_broadcast_listen_setting == checked: + log.debug('(UDP:{port}) No change to status - skipping'.format(port=self.port)) + return + self.udp_broadcast_listen_setting = checked + if self.udp_broadcast_listen_setting: + if self.state() == self.ListeningState: + log.debug('(UDP:{port}) Already listening - skipping') + return + self.bind(self.port) + log.debug('(UDP:{port}) Listening'.format(port=self.port)) + else: + # Close socket + self.udp_stop() + class PJLinkCommands(object): """ diff --git a/openlp/core/projectors/tab.py b/openlp/core/projectors/tab.py index 2407bad66..fa76e1c25 100644 --- a/openlp/core/projectors/tab.py +++ b/openlp/core/projectors/tab.py @@ -27,6 +27,7 @@ import logging from PyQt5 import QtWidgets from openlp.core.common.i18n import UiStrings, translate +from openlp.core.common.registry import Registry from openlp.core.common.settings import Settings from openlp.core.lib.settingstab import SettingsTab from openlp.core.ui.icons import UiIcons @@ -47,8 +48,11 @@ class ProjectorTab(SettingsTab): :param parent: Parent widget """ self.icon_path = UiIcons().projector + self.udp_listeners = {} # Key on port number projector_translated = translate('OpenLP.ProjectorTab', 'Projector') super(ProjectorTab, self).__init__(parent, 'Projector', projector_translated) + Registry().register_function('udp_broadcast_add', self.add_udp_listener) + Registry().register_function('udp_broadcast_remove', self.remove_udp_listener) def setupUi(self): """ @@ -90,6 +94,10 @@ class ProjectorTab(SettingsTab): self.connect_box_layout.addRow(self.dialog_type_label, self.dialog_type_combo_box) self.left_layout.addStretch() self.dialog_type_combo_box.activated.connect(self.on_dialog_type_combo_box_changed) + # Enable/disable listening on UDP ports for PJLink2 broadcasts + self.udp_broadcast_listen = QtWidgets.QCheckBox(self.connect_box) + self.udp_broadcast_listen.setObjectName('udp_broadcast_listen') + self.connect_box_layout.addRow(self.udp_broadcast_listen) # Connect on LKUP packet received (PJLink v2+ only) self.connect_on_linkup = QtWidgets.QCheckBox(self.connect_box) self.connect_on_linkup.setObjectName('connect_on_linkup') @@ -116,6 +124,9 @@ class ProjectorTab(SettingsTab): translate('OpenLP.ProjectorTab', 'Single dialog box')) self.connect_on_linkup.setText( translate('OpenLP.ProjectorTab', 'Connect to projector when LINKUP received (v2 only)')) + self.udp_broadcast_listen.setText( + translate('OpenLP.ProjectorTab', 'Enable listening for PJLink2 broadcast messages')) + log.debug('PJLink settings tab initialized') def load(self): """ @@ -125,6 +136,7 @@ class ProjectorTab(SettingsTab): self.socket_timeout_spin_box.setValue(Settings().value('projector/socket timeout')) self.socket_poll_spin_box.setValue(Settings().value('projector/poll time')) self.dialog_type_combo_box.setCurrentIndex(Settings().value('projector/source dialog type')) + self.udp_broadcast_listen.setChecked(Settings().value('projector/udp broadcast listen')) self.connect_on_linkup.setChecked(Settings().value('projector/connect when LKUP received')) def save(self): @@ -136,6 +148,41 @@ class ProjectorTab(SettingsTab): Settings().setValue('projector/poll time', self.socket_poll_spin_box.value()) Settings().setValue('projector/source dialog type', self.dialog_type_combo_box.currentIndex()) Settings().setValue('projector/connect when LKUP received', self.connect_on_linkup.isChecked()) + Settings().setValue('projector/udp broadcast listen', self.udp_broadcast_listen.isChecked()) + self.call_udp_listener() def on_dialog_type_combo_box_changed(self): self.dialog_type = self.dialog_type_combo_box.currentIndex() + + def add_udp_listener(self, port, callback): + """ + Add new UDP listener to list + """ + if port in self.udp_listeners: + log.warning('Port {port} already in list - not adding'.format(port=port)) + return + self.udp_listeners[port] = callback + log.debug('PJLinkSettings: new callback list: {port}'.format(port=self.udp_listeners.keys())) + + def remove_udp_listener(self, port): + """ + Remove UDP listener from list + """ + if port not in self.udp_listeners: + log.warning('Port {port} not in list - ignoring'.format(port=port)) + return + # Turn off listener before deleting + self.udp_listeners[port](checked=False) + del self.udp_listeners[port] + log.debug('PJLinkSettings: new callback list: {port}'.format(port=self.udp_listeners.keys())) + + def call_udp_listener(self): + """ + Call listeners to update UDP listen setting + """ + if len(self.udp_listeners) < 1: + log.warning('PJLinkSettings: No callers - returning') + return + log.debug('PJLinkSettings: Calling UDP listeners') + for call in self.udp_listeners: + self.udp_listeners[call](checked=self.udp_broadcast_listen.isChecked()) diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py index cc3cd862a..6ad6eeeeb 100644 --- a/openlp/core/ui/aboutdialog.py +++ b/openlp/core/ui/aboutdialog.py @@ -112,8 +112,8 @@ class UiAboutDialog(object): 'Andreas "googol" Preikschat', 'Ken "alisonken1" Roberts', 'Raoul "superfly" Snyman', 'Jonathan "springermac" Springer', 'Philip "Phill" Ridout'] contributors = ['Stuart "sibecker" Becker', 'Gerald "jerryb" Britton', 'Jonathan "gushie" Corwin', - 'Samuel "MrGamgee" Findlay', 'Michael "cocooncrash" Gorven', 'Scott "sguerrieri" Guerrieri', - 'Simon Hanna', 'Chris Hill', + 'Samuel "MrGamgee" Findlay', 'Bastian Germann', 'Michael "cocooncrash" Gorven', + 'Scott "sguerrieri" Guerrieri', 'Simon Hanna', 'Chris Hill', 'Matthias "matthub" Hub', 'Meinert "m2j" Jordan', 'Ian Knightly' 'Armin "orangeshirt" K\xf6hler', 'Rafael "rafaellerm" Lerm', 'Gabriel loo', 'Erik "luen" Lundin', 'Edwin "edwinlunando" Lunando', diff --git a/openlp/core/ui/fonts/LICENSE b/openlp/core/ui/fonts/LICENSE new file mode 100644 index 000000000..8ccfa91c1 --- /dev/null +++ b/openlp/core/ui/fonts/LICENSE @@ -0,0 +1,94 @@ +Copyright Dave Gandy 2016. All rights reserved +with Reserved Font Name FontAwesome + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index 5ca0f45c8..f1933aba3 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -52,10 +52,6 @@ __default_settings__ = { 'bibles/display new chapter': False, 'bibles/second bibles': True, 'bibles/primary bible': '', - 'bibles/proxy name': '', - 'bibles/proxy address': '', - 'bibles/proxy username': '', - 'bibles/proxy password': '', 'bibles/bible theme': '', 'bibles/verse separator': '', 'bibles/range separator': '', diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index d32711b62..912ab26e5 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -210,22 +210,22 @@ class BibleImportForm(OpenLPWizard): self.open_song_layout.addRow(self.open_song_file_label, self.open_song_path_edit) self.open_song_layout.setItem(1, QtWidgets.QFormLayout.LabelRole, self.spacer) self.select_stack.addWidget(self.open_song_widget) - self.web_tab_widget = QtWidgets.QTabWidget(self.select_page) - self.web_tab_widget.setObjectName('WebTabWidget') + self.web_widget = QtWidgets.QWidget(self.select_page) + self.web_widget.setObjectName('WebWidget') self.web_bible_tab = QtWidgets.QWidget() self.web_bible_tab.setObjectName('WebBibleTab') - self.web_bible_layout = QtWidgets.QFormLayout(self.web_bible_tab) + self.web_bible_layout = QtWidgets.QFormLayout(self.web_widget) self.web_bible_layout.setObjectName('WebBibleLayout') - self.web_update_label = QtWidgets.QLabel(self.web_bible_tab) + self.web_update_label = QtWidgets.QLabel(self.web_widget) self.web_update_label.setObjectName('WebUpdateLabel') self.web_bible_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.web_update_label) - self.web_update_button = QtWidgets.QPushButton(self.web_bible_tab) + self.web_update_button = QtWidgets.QPushButton(self.web_widget) self.web_update_button.setObjectName('WebUpdateButton') self.web_bible_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.web_update_button) - self.web_source_label = QtWidgets.QLabel(self.web_bible_tab) + self.web_source_label = QtWidgets.QLabel(self.web_widget) self.web_source_label.setObjectName('WebSourceLabel') self.web_bible_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.web_source_label) - self.web_source_combo_box = QtWidgets.QComboBox(self.web_bible_tab) + self.web_source_combo_box = QtWidgets.QComboBox(self.web_widget) self.web_source_combo_box.setObjectName('WebSourceComboBox') self.web_source_combo_box.addItems(['', '', '']) self.web_source_combo_box.setEnabled(False) @@ -243,31 +243,7 @@ class BibleImportForm(OpenLPWizard): self.web_progress_bar.setObjectName('WebTranslationProgressBar') self.web_progress_bar.setVisible(False) self.web_bible_layout.setWidget(3, QtWidgets.QFormLayout.SpanningRole, self.web_progress_bar) - self.web_tab_widget.addTab(self.web_bible_tab, '') - self.web_proxy_tab = QtWidgets.QWidget() - self.web_proxy_tab.setObjectName('WebProxyTab') - self.web_proxy_layout = QtWidgets.QFormLayout(self.web_proxy_tab) - self.web_proxy_layout.setObjectName('WebProxyLayout') - self.web_server_label = QtWidgets.QLabel(self.web_proxy_tab) - self.web_server_label.setObjectName('WebServerLabel') - self.web_proxy_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.web_server_label) - self.web_server_edit = QtWidgets.QLineEdit(self.web_proxy_tab) - self.web_server_edit.setObjectName('WebServerEdit') - self.web_proxy_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.web_server_edit) - self.web_user_label = QtWidgets.QLabel(self.web_proxy_tab) - self.web_user_label.setObjectName('WebUserLabel') - self.web_proxy_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.web_user_label) - self.web_user_edit = QtWidgets.QLineEdit(self.web_proxy_tab) - self.web_user_edit.setObjectName('WebUserEdit') - self.web_proxy_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.web_user_edit) - self.web_password_label = QtWidgets.QLabel(self.web_proxy_tab) - self.web_password_label.setObjectName('WebPasswordLabel') - self.web_proxy_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.web_password_label) - self.web_password_edit = QtWidgets.QLineEdit(self.web_proxy_tab) - self.web_password_edit.setObjectName('WebPasswordEdit') - self.web_proxy_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.web_password_edit) - self.web_tab_widget.addTab(self.web_proxy_tab, '') - self.select_stack.addWidget(self.web_tab_widget) + self.select_stack.addWidget(self.web_widget) self.zefania_widget = QtWidgets.QWidget(self.select_page) self.zefania_widget.setObjectName('ZefaniaWidget') self.zefania_layout = QtWidgets.QFormLayout(self.zefania_widget) @@ -427,14 +403,6 @@ class BibleImportForm(OpenLPWizard): self.web_source_combo_box.setItemText(WebDownload.Bibleserver, translate('BiblesPlugin.ImportWizardForm', 'Bibleserver')) self.web_translation_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible:')) - self.web_tab_widget.setTabText(self.web_tab_widget.indexOf(self.web_bible_tab), - translate('BiblesPlugin.ImportWizardForm', 'Download Options')) - self.web_server_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Server:')) - self.web_user_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Username:')) - self.web_password_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Password:')) - self.web_tab_widget.setTabText( - self.web_tab_widget.indexOf(self.web_proxy_tab), translate('BiblesPlugin.ImportWizardForm', - 'Proxy Server (Optional)')) self.sword_bible_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bibles:')) self.sword_folder_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD data folder:')) self.sword_zipfile_label.setText(translate('BiblesPlugin.ImportWizardForm', 'SWORD zip-file:')) @@ -609,11 +577,10 @@ class BibleImportForm(OpenLPWizard): self.web_update_button.setEnabled(False) self.web_progress_bar.setVisible(True) self.web_progress_bar.setValue(0) - proxy_server = self.field('proxy_server') # TODO: Where does critical_error_message_box get %s string from? - for (download_type, extractor) in ((WebDownload.Crosswalk, CWExtract(proxy_server)), - (WebDownload.BibleGateway, BGExtract(proxy_server)), - (WebDownload.Bibleserver, BSExtract(proxy_server))): + for (download_type, extractor) in ((WebDownload.Crosswalk, CWExtract()), + (WebDownload.BibleGateway, BGExtract()), + (WebDownload.Bibleserver, BSExtract())): try: bibles = extractor.get_bibles_from_http() except (urllib.error.URLError, ConnectionError) as err: @@ -679,9 +646,6 @@ class BibleImportForm(OpenLPWizard): self.select_page.registerField('web_biblename', self.web_translation_combo_box) self.select_page.registerField('sword_folder_path', self.sword_folder_path_edit, 'path', PathEdit.pathChanged) self.select_page.registerField('sword_zip_path', self.sword_zipfile_path_edit, 'path', PathEdit.pathChanged) - self.select_page.registerField('proxy_server', self.web_server_edit) - self.select_page.registerField('proxy_username', self.web_user_edit) - self.select_page.registerField('proxy_password', self.web_password_edit) self.license_details_page.registerField('license_version', self.version_name_edit) self.license_details_page.registerField('license_copyright', self.copyright_edit) self.license_details_page.registerField('license_permissions', self.permissions_edit) @@ -706,9 +670,6 @@ class BibleImportForm(OpenLPWizard): self.setField('sword_zip_path', None) self.setField('web_location', WebDownload.Crosswalk) self.setField('web_biblename', self.web_translation_combo_box.currentIndex()) - self.setField('proxy_server', settings.value('proxy address')) - self.setField('proxy_username', settings.value('proxy username')) - self.setField('proxy_password', settings.value('proxy password')) self.setField('license_version', self.version_name_edit.text()) self.version_name_edit.setPlaceholderText(UiStrings().RequiredShowInFooter) self.setField('license_copyright', self.copyright_edit.text()) @@ -765,9 +726,6 @@ class BibleImportForm(OpenLPWizard): BibleFormat.WebDownload, name=license_version, download_source=WebDownload.Names[download_location], download_name=bible, - proxy_server=self.field('proxy_server'), - proxy_username=self.field('proxy_username'), - proxy_password=self.field('proxy_password'), language_id=language_id ) elif bible_type == BibleFormat.Zefania: diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index 040115d56..e42fed5c6 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -82,20 +82,19 @@ def init_schema(url): meta_table = Table('metadata', metadata, Column('key', types.Unicode(255), primary_key=True, index=True), - Column('value', types.Unicode(255)),) + Column('value', types.Unicode(255))) book_table = Table('book', metadata, Column('id', types.Integer, primary_key=True), Column('book_reference_id', types.Integer, index=True), Column('testament_reference_id', types.Integer), - Column('name', types.Unicode(50), index=True),) + Column('name', types.Unicode(50), index=True)) verse_table = Table('verse', metadata, Column('id', types.Integer, primary_key=True, index=True), - Column('book_id', types.Integer, ForeignKey( - 'book.id'), index=True), + Column('book_id', types.Integer, ForeignKey('book.id'), index=True), Column('chapter', types.Integer, index=True), Column('verse', types.Integer, index=True), - Column('text', types.UnicodeText, index=True),) + Column('text', types.UnicodeText, index=True)) try: class_mapper(BibleMeta) diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index 4eee26256..c624210be 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/http.py @@ -94,9 +94,8 @@ class BGExtract(RegistryProperties): """ NAME = 'BibleGateway' - def __init__(self, proxy_url=None): - log.debug('BGExtract.init(proxy_url="{url}")'.format(url=proxy_url)) - self.proxy_url = proxy_url + def __init__(self): + log.debug('BGExtract.__init__()') socket.setdefaulttimeout(30) def _remove_elements(self, parent, tag, class_=None): @@ -358,9 +357,8 @@ class BSExtract(RegistryProperties): """ NAME = 'BibleServer' - def __init__(self, proxy_url=None): - log.debug('BSExtract.init("{url}")'.format(url=proxy_url)) - self.proxy_url = proxy_url + def __init__(self): + log.debug('BSExtract.__init__()') socket.setdefaulttimeout(30) def get_bible_chapter(self, version, book_name, chapter): @@ -461,9 +459,8 @@ class CWExtract(RegistryProperties): """ NAME = 'Crosswalk' - def __init__(self, proxy_url=None): - log.debug('CWExtract.init("{url}")'.format(url=proxy_url)) - self.proxy_url = proxy_url + def __init__(self): + log.debug('CWExtract.__init__()') socket.setdefaulttimeout(30) def get_bible_chapter(self, version, book_name, chapter): @@ -595,19 +592,9 @@ class HTTPBible(BibleImport, RegistryProperties): 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. - self.proxy_server = None - self.proxy_username = None - self.proxy_password = None self.language_id = None if 'path' in kwargs: self.path = kwargs['path'] - if 'proxy_server' in kwargs: - self.proxy_server = kwargs['proxy_server'] - if 'proxy_username' in kwargs: - self.proxy_username = kwargs['proxy_username'] - if 'proxy_password' in kwargs: - self.proxy_password = kwargs['proxy_password'] if 'language_id' in kwargs: self.language_id = kwargs['language_id'] @@ -621,20 +608,12 @@ class HTTPBible(BibleImport, RegistryProperties): 'Registering Bible and loading books...')) self.save_meta('download_source', self.download_source) self.save_meta('download_name', self.download_name) - if self.proxy_server: - self.save_meta('proxy_server', self.proxy_server) - if self.proxy_username: - # Store the proxy userid. - self.save_meta('proxy_username', self.proxy_username) - if self.proxy_password: - # Store the proxy password. - self.save_meta('proxy_password', self.proxy_password) if self.download_source.lower() == 'crosswalk': - handler = CWExtract(self.proxy_server) + handler = CWExtract() elif self.download_source.lower() == 'biblegateway': - handler = BGExtract(self.proxy_server) + handler = BGExtract() elif self.download_source.lower() == 'bibleserver': - handler = BSExtract(self.proxy_server) + handler = BSExtract() books = handler.get_books_from_http(self.download_name) if not books: log.error('Importing books from {source} - download name: "{name}" ' @@ -722,11 +701,11 @@ class HTTPBible(BibleImport, RegistryProperties): log.debug('HTTPBible.get_chapter("{book}", "{chapter}")'.format(book=book, chapter=chapter)) log.debug('source = {source}'.format(source=self.download_source)) if self.download_source.lower() == 'crosswalk': - handler = CWExtract(self.proxy_server) + handler = CWExtract() elif self.download_source.lower() == 'biblegateway': - handler = BGExtract(self.proxy_server) + handler = BGExtract() elif self.download_source.lower() == 'bibleserver': - handler = BSExtract(self.proxy_server) + handler = BSExtract() return handler.get_bible_chapter(self.download_name, book, chapter) def get_books(self): diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 1e1b4243d..fce411870 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -116,7 +116,6 @@ class BibleManager(LogMixin, RegistryProperties): self.web = 'Web' self.db_cache = None self.path = AppLocation.get_section_data_path(self.settings_section) - self.proxy_name = Settings().value(self.settings_section + '/proxy name') self.suffix = '.sqlite' self.import_wizard = None self.reload_bibles() @@ -149,11 +148,8 @@ class BibleManager(LogMixin, RegistryProperties): if self.db_cache[name].is_web_bible: source = self.db_cache[name].get_object(BibleMeta, 'download_source') download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value - meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server') web_bible = HTTPBible(self.parent, path=self.path, file=file_path, download_source=source.value, download_name=download_name) - if meta_proxy: - web_bible.proxy_server = meta_proxy.value self.db_cache[name] = web_bible log.debug('Bibles reloaded') diff --git a/openlp/plugins/bibles/lib/upgrade.py b/openlp/plugins/bibles/lib/upgrade.py index c53f9d324..763e40c6b 100644 --- a/openlp/plugins/bibles/lib/upgrade.py +++ b/openlp/plugins/bibles/lib/upgrade.py @@ -24,8 +24,16 @@ The :mod:`upgrade` module provides a way for the database and schema that is the """ import logging +from PyQt5 import QtWidgets +from sqlalchemy import Table +from sqlalchemy.sql.expression import delete, select + +from openlp.core.common.i18n import translate +from openlp.core.common.settings import ProxyMode, Settings +from openlp.core.lib.db import get_upgrade_op + log = logging.getLogger(__name__) -__version__ = 1 +__version__ = 2 # TODO: When removing an upgrade path the ftw-data needs updating to the minimum supported version @@ -36,3 +44,48 @@ def upgrade_1(session, metadata): This upgrade renamed a number of keys to a single naming convention. """ log.info('No upgrades to perform') + + +def upgrade_2(session, metadata): + """ + Remove the individual proxy settings, after the implementation of central proxy settings. + Added in 2.5 (3.0 development) + """ + settings = Settings() + op = get_upgrade_op(session) + metadata_table = Table('metadata', metadata, autoload=True) + proxy, = session.execute(select([metadata_table.c.value], metadata_table.c.key == 'proxy_server')).first() or ('', ) + if proxy and not \ + (proxy == settings.value('advanced/proxy http') or proxy == settings.value('advanced/proxy https')): + http_proxy = '' + https_proxy = '' + name, = session.execute(select([metadata_table.c.value], metadata_table.c.key == 'name')).first() + msg_box = QtWidgets.QMessageBox() + msg_box.setText(translate('BiblesPlugin', f'The proxy server {proxy} was found in the bible {name}.
' + f'Would you like to set it as the proxy for OpenLP?')) + msg_box.setIcon(QtWidgets.QMessageBox.Question) + msg_box.addButton(QtWidgets.QMessageBox.No) + http_button = msg_box.addButton('http', QtWidgets.QMessageBox.ActionRole) + both_button = msg_box.addButton(translate('BiblesPlugin', 'both'), QtWidgets.QMessageBox.ActionRole) + https_button = msg_box.addButton('https', QtWidgets.QMessageBox.ActionRole) + msg_box.setDefaultButton(both_button) + msg_box.exec() + + clicked_button = msg_box.clickedButton() + if clicked_button in [http_button, both_button]: + http_proxy = proxy + settings.setValue('advanced/proxy http', proxy) + if clicked_button in [https_button, both_button]: + https_proxy = proxy + settings.setValue('advanced/proxy https', proxy) + if http_proxy or https_proxy: + username, = session.execute( + select([metadata_table.c.value], metadata_table.c.key == 'proxy_username')).first() + proxy, = session.execute(select([metadata_table.c.value], metadata_table.c.key == 'proxy_password')).first() + settings.setValue('advanced/proxy username', username) + settings.setValue('advanced/proxy password', proxy) + settings.setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY) + + op.execute(delete(metadata_table, metadata_table.c.key == 'proxy_server')) + op.execute(delete(metadata_table, metadata_table.c.key == 'proxy_username')) + op.execute(delete(metadata_table, metadata_table.c.key == 'proxy_password')) diff --git a/openlp.py b/run_openlp.py similarity index 99% rename from openlp.py rename to run_openlp.py index 38e270a9d..89f89db61 100755 --- a/openlp.py +++ b/run_openlp.py @@ -42,7 +42,7 @@ def set_up_fault_handling(): faulthandler.enable((AppLocation.get_directory(AppLocation.CacheDir) / 'error.log').open('wb')) -if __name__ == '__main__': +def start(): """ Instantiate and run the application. """ @@ -58,3 +58,7 @@ if __name__ == '__main__': if is_macosx(): sys.argv = [x for x in sys.argv if not x.startswith('-psn')] main() + + +if __name__ == '__main__': + start() diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 5948253b9..bef8cb5e5 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -11,11 +11,8 @@ environment: install: # Install dependencies from pypi - - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==4.0.8 psycopg2 pypiwin32==219 pyenchant websockets asyncio waitress six webob requests QtAwesome" - # Install mysql dependency - - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df" - # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) - - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/lxml-3.6.4-cp34-cp34m-win32.whl" + - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python nose mock pyodbc==4.0.8 psycopg2 pypiwin32==219 pyenchant pymediainfo websockets asyncio waitress six webob requests QtAwesome" + # Download and install pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.5-cp34-cp34m-win32.whl" # Download and install PyQt5 - appveyor DownloadFile http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index c8743ab8e..53dfd46e7 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -33,24 +33,18 @@ import os import sys from distutils.version import LooseVersion -# If we try to import uno before nose this will create a warning. Just try to import nose first to suppress the warning. -try: - import nose -except ImportError: - nose = None - IS_WIN = sys.platform.startswith('win') IS_LIN = sys.platform.startswith('lin') IS_MAC = sys.platform.startswith('dar') VERS = { - 'Python': '3.4', + 'Python': '3.6', 'PyQt5': '5.0', 'Qt5': '5.0', + 'pymediainfo': '2.2', 'sqlalchemy': '0.5', - # pyenchant 1.6 required on Windows - 'enchant': '1.6' if IS_WIN else '1.3' + 'enchant': '1.6' } # pywin32 @@ -58,7 +52,6 @@ WIN32_MODULES = [ 'win32com', 'win32ui', 'pywintypes', - 'pyodbc', 'icu', ] @@ -84,19 +77,16 @@ MODULES = [ 'PyQt5.QtTest', 'PyQt5.QtWebKit', 'PyQt5.QtMultimedia', + 'pymediainfo', + 'appdirs', 'sqlalchemy', 'alembic', - 'sqlite3', 'lxml', 'chardet', - 'enchant', 'bs4', 'mako', - 'uno', 'websockets', - 'asyncio', 'waitress', - 'six', 'webob', 'requests', 'qtawesome', @@ -105,12 +95,17 @@ MODULES = [ OPTIONAL_MODULES = [ - ('mysql.connector', '(MySQL support)', True), - ('psycopg2', '(PostgreSQL support)', True), - ('nose', '(testing framework)', True), - ('mock', '(testing module)', sys.version_info[1] < 3), - ('jenkins', '(access jenkins api - package name: jenkins-webapi)', True), - ('pysword', '(import SWORD bibles)', True), + ('mysql.connector', '(MySQL support)'), + ('pyodbc', '(ODBC support)'), + ('psycopg2', '(PostgreSQL support)'), + ('enchant', '(spell checker)'), + ('pysword', '(import SWORD bibles)'), + ('uno', '(LibreOffice/OpenOffice support)'), + # development/testing modules + ('jenkins', '(access jenkins api)'), + ('launchpadlib', '(launchpad script support)'), + ('nose2', '(testing framework)'), + ('pylint', '(linter)') ] w = sys.stdout.write @@ -239,8 +234,7 @@ def main(): check_module(m) print('Checking for optional modules...') for m in OPTIONAL_MODULES: - if m[2]: - check_module(m[0], text=m[1]) + check_module(m[0], text=m[1]) if IS_WIN: print('Checking for Windows specific modules...') for m in WIN32_MODULES: diff --git a/setup.py b/setup.py index a28fa0300..bdd4c6e94 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 @@ -21,6 +22,7 @@ ############################################################################### import re +import sys from setuptools import setup, find_packages from subprocess import Popen, PIPE @@ -109,6 +111,34 @@ except Exception: finally: ver_file.close() +requires = [ + 'alembic', + 'appdirs', + 'beautifulsoup4', + 'chardet', + 'lxml', + 'Mako', + 'pymediainfo >= 2.2', + 'PyQt5', + 'QtAwesome', + 'requests', + 'SQLAlchemy >= 0.5', + 'waitress', + 'WebOb', + 'websockets' +] +if sys.platform.startswith('win'): + requires.extend([ + 'PyICU', + 'pywin32' + ]) +elif sys.platform.startswith('darwin'): + requires.extend([ + 'pyobjc', + 'pyobjc-framework-Cocoa' + ]) +elif sys.platform.startswith('linux'): + requires.append('dbus-python') setup( name='OpenLP', @@ -156,18 +186,25 @@ using a computer and a data projector.""", keywords='open source church presentation lyrics projection song bible display project', author='Raoul Snyman', author_email='raoulsnyman@openlp.org', - url='http://openlp.org/', + url='https://openlp.org/', license='GNU General Public License', - packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), - scripts=['openlp.py'], + packages=find_packages(exclude=['ez_setup', 'tests']), + py_modules=['run_openlp'], include_package_data=True, zip_safe=False, - install_requires=[ - # -*- Extra requirements: -*- - 'sqlalchemy', - 'alembic' - ], - entry_points=""" - # -*- Entry points: -*- - """ + python_requires='>=3.6', + install_requires=requires, + extras_require={ + 'mysql': ['mysql-connector-python'], + 'odbc': ['pyodbc'], + 'postgresql': ['psycopg2'], + 'spellcheck': ['pyenchant >= 1.6'], + 'sword-bibles': ['pysword'], + # Required for scripts/*.py: + 'jenkins': ['python-jenkins'], + 'launchpad': ['launchpadlib'] + }, + tests_require=['nose2', 'PyICU', 'pylint', 'pyodbc', 'pysword'], + test_suite='nose2.collector.collector', + entry_points={'gui_scripts': ['openlp = run_openlp:start']} ) diff --git a/tests/README.txt b/tests/README.txt index 493b5ccb5..865d22d1b 100644 --- a/tests/README.txt +++ b/tests/README.txt @@ -8,10 +8,10 @@ Prerequisites In order to run the unit tests, you will need the following Python packages/libraries installed: - - Mock - - Nose + - pytest + - pylint3 -On Ubuntu you can simple install the python-mock and python-nose packages. Most other distributions will also have these +On Ubuntu you can simple install the python3-pytest and pylint3 packages. Most other distributions will also have these packages. On Windows and Mac OS X you will need to use ``pip`` or ``easy_install`` to install these packages. Running the Tests @@ -19,16 +19,12 @@ Running the Tests To run the tests, navigate to the root directory of the OpenLP project, and then run the following command:: - nosetests -v tests + pytest -v tests Or, to run only the functional tests, run the following command:: - nosetests -v tests/functional + pytest -v tests/functional Or, to run only a particular test suite within a file, run the following command:: - nosetests -v tests/functional/test_applocation.py - -Finally, to only run a particular test, run the following command:: - - nosetests -v tests/functional/test_applocation.py:TestAppLocation.get_frozen_path_test + pytest -v tests/functional/openlp_core/test_app.py diff --git a/tests/functional/openlp_core/common/test_network_interfaces.py b/tests/functional/openlp_core/common/test_network_interfaces.py deleted file mode 100644 index d1547bd0a..000000000 --- a/tests/functional/openlp_core/common/test_network_interfaces.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Functional tests to test calls for network interfaces. -""" -from unittest import TestCase -from unittest.mock import MagicMock, call, patch - -import openlp.core.common -from openlp.core.common import get_local_ip4 - -from tests.helpers.testmixin import TestMixin - -lo_address_attrs = {'isValid.return_value': True, - 'flags.return_value': True, - 'InterfaceFlags.return_value': True, - 'name.return_value': 'lo', - 'broadcast.toString.return_value': '127.0.0.255', - 'netmask.toString.return_value': '255.0.0.0', - 'prefixLength.return_value': 8, - 'ip.protocol.return_value': True} - - -class TestInterfaces(TestCase, TestMixin): - """ - A test suite to test out functions/methods that use network interface(s). - """ - def setUp(self): - """ - Create an instance and a few example actions. - """ - self.build_settings() - - self.ip4_lo_address = MagicMock() - self.ip4_lo_address.configure_mock(**lo_address_attrs) - - def tearDown(self): - """ - Clean up - """ - self.destroy_settings() - - @patch.object(openlp.core.common, 'log') - def test_ip4_no_interfaces(self, mock_log): - """ - Test no interfaces available - """ - # GIVEN: Test environment - call_warning = [call('No active IPv4 network interfaces detected')] - - with patch('openlp.core.common.QNetworkInterface') as mock_newtork_interface: - mock_newtork_interface.allInterfaces.return_value = [] - - # WHEN: get_local_ip4 is called - ifaces = get_local_ip4() - - # THEN: There should not be any interfaces detected - assert not ifaces, 'There should have been no active interfaces' - mock_log.warning.assert_has_calls(call_warning) diff --git a/tests/functional/openlp_plugins/bibles/test_upgrade.py b/tests/functional/openlp_plugins/bibles/test_upgrade.py new file mode 100644 index 000000000..1e2520391 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_upgrade.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2018 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 upgrade submodule of the Bibles plugin. +""" +import os +import shutil +from pathlib import Path +from tempfile import mkdtemp +from unittest import TestCase +from unittest.mock import MagicMock, call, patch + +from PyQt5 import QtWidgets +from sqlalchemy import create_engine + +from openlp.core.common.settings import ProxyMode +from openlp.core.lib.db import upgrade_db +from openlp.plugins.bibles.lib import upgrade +from tests.helpers.testmixin import TestMixin +from tests.utils.constants import RESOURCE_PATH + + +class TestUpgrade(TestCase, TestMixin): + """ + Test the `upgrade_2` function in the :mod:`upgrade` module when the db does not contains proxy metadata + """ + + def setUp(self): + """ + Setup for tests + """ + self.tmp_path = Path(mkdtemp()) + db_path = RESOURCE_PATH / 'bibles' / 'web-bible-2.4.6-v1.sqlite' + db_tmp_path = self.tmp_path / 'web-bible-2.4.6-v1.sqlite' + shutil.copyfile(db_path, db_tmp_path) + self.db_url = 'sqlite:///' + str(db_tmp_path) + + patched_settings = patch('openlp.plugins.bibles.lib.upgrade.Settings') + self.mocked_settings = patched_settings.start() + self.addCleanup(patched_settings.stop) + self.mocked_settings_instance = MagicMock() + self.mocked_settings.return_value = self.mocked_settings_instance + + patched_message_box = patch('openlp.plugins.bibles.lib.upgrade.QtWidgets.QMessageBox') + self.mocked_message_box = patched_message_box.start() + self.addCleanup(patched_message_box.stop) + + def tearDown(self): + """ + Clean up after tests + """ + # Ignore errors since windows can have problems with locked files + shutil.rmtree(self.tmp_path, ignore_errors=True) + + def test_upgrade_2_none_selected(self): + """ + Test that upgrade 2 completes properly when the user chooses not to use a proxy ('No') + """ + # GIVEN: An version 1 web bible with proxy settings + + # WHEN: Calling upgrade_db and the user has 'clicked' the 'No' button + upgrade_db(self.db_url, upgrade) + + # THEN: The proxy meta data should have been removed, and the version should have been changed to version 2 + self.mocked_message_box.assert_not_called() + engine = create_engine(self.db_url) + conn = engine.connect() + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + + +class TestProxyMetaUpgrade(TestCase, TestMixin): + """ + Test the `upgrade_2` function in the :mod:`upgrade` module when the db contains proxy metadata + """ + + def setUp(self): + """ + Setup for tests + """ + self.tmp_path = Path(mkdtemp()) + db_path = RESOURCE_PATH / 'bibles' / 'web-bible-2.4.6-proxy-meta-v1.sqlite' + db_tmp_path = self.tmp_path / 'web-bible-2.4.6-proxy-meta-v1.sqlite' + shutil.copyfile(db_path, db_tmp_path) + self.db_url = 'sqlite:///' + str(db_tmp_path) + + patched_settings = patch('openlp.plugins.bibles.lib.upgrade.Settings') + self.mocked_settings = patched_settings.start() + self.addCleanup(patched_settings.stop) + self.mocked_settings_instance = MagicMock() + self.mocked_settings.return_value = self.mocked_settings_instance + + patched_message_box = patch('openlp.plugins.bibles.lib.upgrade.QtWidgets.QMessageBox') + mocked_message_box = patched_message_box.start() + self.addCleanup(patched_message_box.stop) + self.mocked_no_button = MagicMock() + self.mocked_http_button = MagicMock() + self.mocked_both_button = MagicMock() + self.mocked_https_button = MagicMock() + self.mocked_message_box_instance = MagicMock( + **{'addButton.side_effect': [self.mocked_no_button, self.mocked_http_button, + self.mocked_both_button, self.mocked_https_button]}) + mocked_message_box.return_value = self.mocked_message_box_instance + + def tearDown(self): + """ + Clean up after tests + """ + # Ignore errors since windows can have problems with locked files + shutil.rmtree(self.tmp_path, ignore_errors=True) + + def test_upgrade_2_none_selected(self): + """ + Test that upgrade 2 completes properly when the user chooses not to use a proxy ('No') + """ + # GIVEN: An version 1 web bible with proxy settings + + # WHEN: Calling upgrade_db and the user has 'clicked' the 'No' button + self.mocked_message_box_instance.clickedButton.return_value = self.mocked_no_button + upgrade_db(self.db_url, upgrade) + + # THEN: The proxy meta data should have been removed, and the version should have been changed to version 2 + engine = create_engine(self.db_url) + conn = engine.connect() + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_server"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_username"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_password"').fetchall()) == 0 + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + self.mocked_settings_instance.setValue.assert_not_called() + + def test_upgrade_2_http_selected(self): + """ + Test that upgrade 2 completes properly when the user chooses to use a HTTP proxy + """ + # GIVEN: An version 1 web bible with proxy settings + + # WHEN: Calling upgrade_db and the user has 'clicked' the 'HTTP' button + self.mocked_message_box_instance.clickedButton.return_value = self.mocked_http_button + upgrade_db(self.db_url, upgrade) + + # THEN: The proxy meta data should have been removed, the version should have been changed to version 2, and the + # proxy server saved to the settings + engine = create_engine(self.db_url) + conn = engine.connect() + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_server"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_username"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_password"').fetchall()) == 0 + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + + assert self.mocked_settings_instance.setValue.call_args_list == [ + call('advanced/proxy http', 'proxy_server'), call('advanced/proxy username', 'proxy_username'), + call('advanced/proxy password', 'proxy_password'), call('advanced/proxy mode', ProxyMode.MANUAL_PROXY)] + + def test_upgrade_2_https_selected(self): + """ + Tcest that upgrade 2 completes properly when the user chooses to use a HTTPS proxy + """ + # GIVEN: An version 1 web bible with proxy settings + + # WHEN: Calling upgrade_db and the user has 'clicked' the 'HTTPS' button + self.mocked_message_box_instance.clickedButton.return_value = self.mocked_https_button + upgrade_db(self.db_url, upgrade) + + # THEN: The proxy settings should have been removed, the version should have been changed to version 2, and the + # proxy server saved to the settings + engine = create_engine(self.db_url) + conn = engine.connect() + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_server"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_username"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_password"').fetchall()) == 0 + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + + assert self.mocked_settings_instance.setValue.call_args_list == [ + call('advanced/proxy https', 'proxy_server'), call('advanced/proxy username', 'proxy_username'), + call('advanced/proxy password', 'proxy_password'), call('advanced/proxy mode', ProxyMode.MANUAL_PROXY)] + + def test_upgrade_2_both_selected(self): + """ + Tcest that upgrade 2 completes properly when the user chooses to use a both HTTP and HTTPS proxies + """ + + # GIVEN: An version 1 web bible with proxy settings + + # WHEN: Calling upgrade_db + self.mocked_message_box_instance.clickedButton.return_value = self.mocked_both_button + upgrade_db(self.db_url, upgrade) + + # THEN: The proxy settings should have been removed, the version should have been changed to version 2, and the + # proxy server saved to the settings + engine = create_engine(self.db_url) + conn = engine.connect() + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_server"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_username"').fetchall()) == 0 + assert len(conn.execute('SELECT * FROM metadata WHERE key = "proxy_password"').fetchall()) == 0 + assert conn.execute('SELECT * FROM metadata WHERE key = "version"').first().value == '2' + + assert self.mocked_settings_instance.setValue.call_args_list == [ + call('advanced/proxy http', 'proxy_server'), call('advanced/proxy https', 'proxy_server'), + call('advanced/proxy username', 'proxy_username'), call('advanced/proxy password', 'proxy_password'), + call('advanced/proxy mode', ProxyMode.MANUAL_PROXY)] diff --git a/tests/openlp_core/common/test_network_interfaces.py b/tests/openlp_core/common/test_network_interfaces.py new file mode 100644 index 000000000..7a049b234 --- /dev/null +++ b/tests/openlp_core/common/test_network_interfaces.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2017 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test calls for network interfaces. +""" +from unittest import TestCase +from unittest.mock import call, patch +from PyQt5.QtCore import QObject +from PyQt5.QtNetwork import QHostAddress, QNetworkAddressEntry, QNetworkInterface + +import openlp.core.common +from openlp.core.common import get_local_ip4 + +from tests.helpers.testmixin import TestMixin + + +class FakeIP4InterfaceEntry(QObject): + """ + Class to face an interface for testing purposes + """ + def __init__(self, name='lo'): + self.my_name = name + if name in ['localhost', 'lo']: + self.my_ip = QNetworkAddressEntry() + self.my_ip.setBroadcast(QHostAddress('255.0.0.0')) + self.my_ip.setIp(QHostAddress('127.0.0.2')) + self.my_ip.setPrefixLength(8) + self.fake_data = {'lo': {'ip': '127.0.0.2', + 'broadcast': '255.0.0.0', + 'netmask': '255.0.0.0', + 'prefix': 8, + 'localnet': '127.0.0.0'}} + else: + # Define a fake real address + self.my_ip = QNetworkAddressEntry() + self.my_ip.setBroadcast(QHostAddress('255.255.255.0')) + self.my_ip.setIp(QHostAddress('127.254.0.2')) + self.my_ip.setPrefixLength(24) + self.fake_data = {self.my_name: {'ip': '127.254.0.2', + 'broadcast': '255.255.255.0', + 'netmask': '255.255.255.0', + 'prefix': 24, + 'localnet': '127.254.0.0'}} + + def addressEntries(self): + """ + Return fake IP address + """ + return [self.my_ip] + + def flags(self): + """ + Return a QFlags enum with IsUp and IsRunning + """ + return (QNetworkInterface.IsUp | QNetworkInterface.IsRunning) + + def name(self): + return self.my_name + + def isValid(self): + return True + + +class TestInterfaces(TestCase, TestMixin): + """ + A test suite to test out functions/methods that use network interface(s). + """ + def setUp(self): + """ + Create an instance and a few example actions. + """ + self.build_settings() + if not hasattr(self, 'fake_lo'): + # Since these shouldn't change, only need to instantiate them the first time + self.fake_lo = FakeIP4InterfaceEntry() + self.fake_localhost = FakeIP4InterfaceEntry(name='localhost') + self.fake_address = FakeIP4InterfaceEntry(name='eth25') + + def tearDown(self): + """ + Clean up + """ + self.destroy_settings() + + @patch.object(openlp.core.common, 'log') + def test_ip4_no_interfaces(self, mock_log): + """ + Test no interfaces available + """ + # GIVEN: Test environment + call_debug = [call('Getting local IPv4 interface(es) information')] + call_warning = [call('No active IPv4 network interfaces detected')] + + # WHEN: get_local_ip4 is called + with patch('openlp.core.common.QNetworkInterface') as mock_network_interface: + mock_network_interface.allInterfaces.return_value = [] + ifaces = get_local_ip4() + + # THEN: There should not be any interfaces detected + mock_log.debug.assert_has_calls(call_debug) + mock_log.warning.assert_has_calls(call_warning) + assert not ifaces, 'There should have been no active interfaces listed' + + @patch.object(openlp.core.common, 'log') + def test_ip4_lo(self, mock_log): + """ + Test get_local_ip4 returns proper dictionary with 'lo' + """ + # GIVEN: Test environment + call_debug = [call('Getting local IPv4 interface(es) information'), + call('Checking for isValid and flags == IsUP | IsRunning'), + call('Checking address(es) protocol'), + call('Checking for protocol == IPv4Protocol'), + call('Getting interface information'), + call('Adding lo to active list')] + call_warning = [call('No active IPv4 interfaces found except localhost')] + + # WHEN: get_local_ip4 is called + with patch('openlp.core.common.QNetworkInterface') as mock_network_interface: + mock_network_interface.allInterfaces.return_value = [self.fake_lo] + ifaces = get_local_ip4() + + # THEN: There should be a fake 'lo' interface + mock_log.debug.assert_has_calls(call_debug) + mock_log.warning.assert_has_calls(call_warning) + assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed" + + @patch.object(openlp.core.common, 'log') + def test_ip4_localhost(self, mock_log): + """ + Test get_local_ip4 returns proper dictionary with 'lo' if interface is 'localhost' + """ + # GIVEN: Test environment + call_debug = [call('Getting local IPv4 interface(es) information'), + call('Checking for isValid and flags == IsUP | IsRunning'), + call('Checking address(es) protocol'), + call('Checking for protocol == IPv4Protocol'), + call('Getting interface information'), + call('Adding localhost to active list'), + call('Renaming windows localhost to lo')] + call_warning = [call('No active IPv4 interfaces found except localhost')] + + # WHEN: get_local_ip4 is called + with patch('openlp.core.common.QNetworkInterface') as mock_network_interface: + mock_network_interface.allInterfaces.return_value = [self.fake_localhost] + ifaces = get_local_ip4() + + # THEN: There should be a fake 'lo' interface + mock_log.debug.assert_has_calls(call_debug) + mock_log.warning.assert_has_calls(call_warning) + assert ifaces == self.fake_lo.fake_data, "There should have been an 'lo' interface listed" + + @patch.object(openlp.core.common, 'log') + def test_ip4_eth25(self, mock_log): + """ + Test get_local_ip4 returns proper dictionary with 'eth25' + """ + # GIVEN: Test environment + call_debug = [call('Getting local IPv4 interface(es) information'), + call('Checking for isValid and flags == IsUP | IsRunning'), + call('Checking address(es) protocol'), + call('Checking for protocol == IPv4Protocol'), + call('Getting interface information'), + call('Adding eth25 to active list')] + call_warning = [] + + # WHEN: get_local_ip4 is called + with patch('openlp.core.common.QNetworkInterface') as mock_network_interface: + mock_network_interface.allInterfaces.return_value = [self.fake_address] + ifaces = get_local_ip4() + + # THEN: There should be a fake 'eth25' interface + mock_log.debug.assert_has_calls(call_debug) + mock_log.warning.assert_has_calls(call_warning) + assert ifaces == self.fake_address.fake_data + + @patch.object(openlp.core.common, 'log') + def test_ip4_lo_eth25(self, mock_log): + """ + Test get_local_ip4 returns proper dictionary with 'eth25' + """ + # GIVEN: Test environment + call_debug = [call('Getting local IPv4 interface(es) information'), + call('Checking for isValid and flags == IsUP | IsRunning'), + call('Checking address(es) protocol'), + call('Checking for protocol == IPv4Protocol'), + call('Getting interface information'), + call('Adding lo to active list'), + call('Checking for isValid and flags == IsUP | IsRunning'), + call('Checking address(es) protocol'), + call('Checking for protocol == IPv4Protocol'), + call('Getting interface information'), + call('Adding eth25 to active list'), + call('Found at least one IPv4 interface, removing localhost')] + call_warning = [] + + # WHEN: get_local_ip4 is called + with patch('openlp.core.common.QNetworkInterface') as mock_network_interface: + mock_network_interface.allInterfaces.return_value = [self.fake_lo, self.fake_address] + ifaces = get_local_ip4() + + # THEN: There should be a fake 'eth25' interface + mock_log.debug.assert_has_calls(call_debug) + mock_log.warning.assert_has_calls(call_warning) + assert ifaces == self.fake_address.fake_data, "There should have been only 'eth25' interface listed" diff --git a/tests/openlp_core/projectors/__init__.py b/tests/openlp_core/projectors/__init__.py index 0a1a5ca7e..64536c41d 100644 --- a/tests/openlp_core/projectors/__init__.py +++ b/tests/openlp_core/projectors/__init__.py @@ -4,7 +4,7 @@ ############################################################################### # OpenLP - Open Source Lyrics Projection # # --------------------------------------------------------------------------- # -# Copyright (c) 2008-2017 OpenLP Developers # +# Copyright (c) 2008-2018 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 # @@ -20,5 +20,5 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -Module-level functions for the projector test suite +:mod tests/openlp_core/projectors: Tests for projector code """ diff --git a/tests/openlp_core/projectors/test_projector_pjlink_udp.py b/tests/openlp_core/projectors/test_projector_pjlink_udp.py index 45c91347f..564e13128 100644 --- a/tests/openlp_core/projectors/test_projector_pjlink_udp.py +++ b/tests/openlp_core/projectors/test_projector_pjlink_udp.py @@ -28,16 +28,45 @@ from unittest import TestCase from unittest.mock import call, patch import openlp.core.projectors.pjlink +from openlp.core.common.registry import Registry from openlp.core.projectors.constants import PJLINK_PORT - +from openlp.core.projectors.tab import ProjectorTab from openlp.core.projectors.pjlink import PJLinkUDP + +from tests.helpers.testmixin import TestMixin from tests.resources.projector.data import TEST1_DATA -class TestPJLinkBase(TestCase): +class TestPJLinkBase(TestCase, TestMixin): """ Tests for the PJLinkUDP class """ + def setUp(self): + """ + Create the UI and setup necessary options + """ + self.setup_application() + self.build_settings() + Registry.create() + """ + with patch('openlp.core.projectors.db.init_url') as mocked_init_url: + if os.path.exists(TEST_DB): + os.unlink(TEST_DB) + mocked_init_url.return_value = 'sqlite:///%s' % TEST_DB + self.projectordb = ProjectorDB() + if not hasattr(self, 'projector_manager'): + self.projector_manager = ProjectorManager(projectordb=self.projectordb) + """ + + def tearDown(self): + """ + Remove test database. + Delete all the C++ objects at the end so that we don't have a segfault. + """ + # self.projectordb.session.close() + self.destroy_settings() + # del self.projector_manager + @patch.object(openlp.core.projectors.pjlink, 'log') def test_get_datagram_data_negative_zero_length(self, mock_log): """ @@ -45,9 +74,9 @@ class TestPJLinkBase(TestCase): """ # GIVEN: Test setup pjlink_udp = PJLinkUDP() - log_warning_calls = [call('(UDP) No data (-1)')] - log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'), - call('(UDP) get_datagram() - Receiving data')] + log_warning_calls = [call('(UDP:4352) No data (-1)')] + log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'), + call('(UDP:4352) get_datagram() - Receiving data')] with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \ patch.object(pjlink_udp, 'readDatagram') as mock_read: mock_datagram.return_value = -1 @@ -67,9 +96,9 @@ class TestPJLinkBase(TestCase): """ # GIVEN: Test setup pjlink_udp = PJLinkUDP() - log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')] - log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'), - call('(UDP) get_datagram() - Receiving data')] + log_warning_calls = [call('(UDP:4352) get_datagram() called when pending data size is 0')] + log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'), + call('(UDP:4352) get_datagram() - Receiving data')] with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \ patch.object(pjlink_udp, 'readDatagram') as mock_read: mock_datagram.return_value = 0 @@ -89,9 +118,9 @@ class TestPJLinkBase(TestCase): """ # GIVEN: Test setup pjlink_udp = PJLinkUDP() - log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')] - log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'), - call('(UDP) get_datagram() - Receiving data')] + log_warning_calls = [call('(UDP:4352) get_datagram() called when pending data size is 0')] + log_debug_calls = [call('(UDP:4352) PJLinkUDP() Initialized'), + call('(UDP:4352) get_datagram() - Receiving data')] with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram: mock_datagram.return_value = 0 @@ -101,3 +130,147 @@ class TestPJLinkBase(TestCase): # THEN: Log entries should be made and method returns mock_log.warning.assert_has_calls(log_warning_calls) mock_log.debug.assert_has_calls(log_debug_calls) + + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_add_udp_listener(self, mock_log): + """ + Test adding UDP listners to PJLink Settings tab + """ + # GIVEN: Initial setup + log_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])')] + log_warning_calls = [] + + pjlink_udp = PJLinkUDP() + settings_tab = ProjectorTab(parent=None) + + # WHEN: add_udp_listener is called with single port + settings_tab.add_udp_listener(port=pjlink_udp.port, callback=pjlink_udp.check_settings) + + # THEN: settings tab should have one entry + assert len(settings_tab.udp_listeners) == 1 + assert pjlink_udp.port in settings_tab.udp_listeners + mock_log.debug.assert_has_calls(log_debug_calls) + mock_log.warning.assert_has_calls(log_warning_calls) + + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_add_udp_listener_multiple_same(self, mock_log): + """ + Test adding second UDP listner with same port to PJLink Settings tab + """ + # GIVEN: Initial setup + log_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])')] + log_warning_calls = [call('Port 4352 already in list - not adding')] + pjlink_udp = PJLinkUDP() + settings_tab = ProjectorTab(parent=None) + settings_tab.add_udp_listener(port=pjlink_udp.port, callback=pjlink_udp.check_settings) + + # WHEN: add_udp_listener is called with second instance same port + settings_tab.add_udp_listener(port=pjlink_udp.port, callback=pjlink_udp.check_settings) + + # THEN: settings tab should have one entry + assert len(settings_tab.udp_listeners) == 1 + assert pjlink_udp.port in settings_tab.udp_listeners + mock_log.debug.assert_has_calls(log_debug_calls) + mock_log.warning.assert_has_calls(log_warning_calls) + + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_add_udp_listener_multiple_different(self, mock_log): + """ + Test adding second UDP listner with different port to PJLink Settings tab + """ + # GIVEN: Initial setup + log_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])')] + log_warning_calls = [] + + settings_tab = ProjectorTab(parent=None) + pjlink_udp1 = PJLinkUDP(port=4352) + settings_tab.add_udp_listener(port=pjlink_udp1.port, callback=pjlink_udp1.check_settings) + + # WHEN: add_udp_listener is called with second instance different port + pjlink_udp2 = PJLinkUDP(port=4353) + settings_tab.add_udp_listener(port=pjlink_udp2.port, callback=pjlink_udp2.check_settings) + + # THEN: settings tab should have two entry + assert len(settings_tab.udp_listeners) == 2 + assert pjlink_udp1.port in settings_tab.udp_listeners + assert pjlink_udp2.port in settings_tab.udp_listeners + mock_log.debug.assert_has_calls(log_debug_calls) + mock_log.warning.assert_has_calls(log_warning_calls) + + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_remove_udp_listener(self, mock_log): + """ + Test removing UDP listners to PJLink Settings tab + """ + # GIVEN: Initial setup + log_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])'), + call('PJLinkSettings: new callback list: dict_keys([])')] + log_warning_calls = [] + + pjlink_udp = PJLinkUDP() + settings_tab = ProjectorTab(parent=None) + settings_tab.add_udp_listener(port=pjlink_udp.port, callback=pjlink_udp.check_settings) + + # WHEN: remove_udp_listener is called with single port + settings_tab.remove_udp_listener(port=pjlink_udp.port) + + # THEN: settings tab should have one entry + assert len(settings_tab.udp_listeners) == 0 + mock_log.debug.assert_has_calls(log_debug_calls) + mock_log.warning.assert_has_calls(log_warning_calls) + + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_remove_udp_listener_multiple_different(self, mock_log): + """ + Test adding second UDP listner with different port to PJLink Settings tab + """ + # GIVEN: Initial setup + log_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])')] + log_warning_calls = [] + + settings_tab = ProjectorTab(parent=None) + pjlink_udp1 = PJLinkUDP(port=4352) + settings_tab.add_udp_listener(port=pjlink_udp1.port, callback=pjlink_udp1.check_settings) + pjlink_udp2 = PJLinkUDP(port=4353) + settings_tab.add_udp_listener(port=pjlink_udp2.port, callback=pjlink_udp2.check_settings) + + # WHEN: remove_udp_listener called for one port + settings_tab.remove_udp_listener(port=4353) + + # THEN: settings tab should have one entry + assert len(settings_tab.udp_listeners) == 1 + assert pjlink_udp1.port in settings_tab.udp_listeners + assert pjlink_udp2.port not in settings_tab.udp_listeners + mock_log.debug.assert_has_calls(log_debug_calls) + mock_log.warning.assert_has_calls(log_warning_calls) + + @patch.object(PJLinkUDP, 'check_settings') + @patch.object(openlp.core.projectors.pjlink, 'log') + @patch.object(openlp.core.projectors.tab, 'log') + def test_pjlinksettings_call_udp_listener(self, mock_tab_log, mock_pjlink_log, mock_check_settings): + """ + Test calling UDP listners in PJLink Settings tab + """ + # GIVEN: Initial setup + tab_debug_calls = [call('PJLink settings tab initialized'), + call('PJLinkSettings: new callback list: dict_keys([4352])'), + call('PJLinkSettings: Calling UDP listeners')] + pjlink_debug_calls = [call.debug('(UDP:4352) PJLinkUDP() Initialized')] + + pjlink_udp = PJLinkUDP() + settings_tab = ProjectorTab(parent=None) + settings_tab.add_udp_listener(port=pjlink_udp.port, callback=pjlink_udp.check_settings) + + # WHEN: calling UDP listener via registry + settings_tab.call_udp_listener() + + # THEN: settings tab should have one entry + assert len(settings_tab.udp_listeners) == 1 + mock_check_settings.assert_called() + mock_tab_log.debug.assert_has_calls(tab_debug_calls) + mock_pjlink_log.assert_has_calls(pjlink_debug_calls) diff --git a/tests/resources/bibles/tests.sqlite b/tests/resources/bibles/tests.sqlite index b6af457d7..a49d3b21a 100644 Binary files a/tests/resources/bibles/tests.sqlite and b/tests/resources/bibles/tests.sqlite differ diff --git a/tests/resources/bibles/web-bible-2.4.6-proxy-meta-v1.sqlite b/tests/resources/bibles/web-bible-2.4.6-proxy-meta-v1.sqlite new file mode 100644 index 000000000..f4030ab53 Binary files /dev/null and b/tests/resources/bibles/web-bible-2.4.6-proxy-meta-v1.sqlite differ diff --git a/tests/resources/bibles/web-bible-2.4.6-v1.sqlite b/tests/resources/bibles/web-bible-2.4.6-v1.sqlite new file mode 100644 index 000000000..6d61b153e Binary files /dev/null and b/tests/resources/bibles/web-bible-2.4.6-v1.sqlite differ