This commit is contained in:
Philip Ridout 2019-03-15 20:56:32 +00:00
commit e1d2c67f33
26 changed files with 144 additions and 87 deletions

View File

@ -7,6 +7,7 @@ cover
.coverage .coverage
coverage coverage
.directory .directory
.vscode
dist dist
*.dll *.dll
documentation/build/doctrees documentation/build/doctrees

View File

@ -706,6 +706,7 @@ class Renderer(RegistryBase, LogMixin, RegistryProperties, DisplayWindow):
else: else:
# The remaining elements do not fit, thus reset the indexes, create a new list and continue. # The remaining elements do not fit, thus reset the indexes, create a new list and continue.
raw_list = raw_list[index + 1:] raw_list = raw_list[index + 1:]
log.debug(raw_list)
raw_list[0] = raw_tags + raw_list[0] raw_list[0] = raw_tags + raw_list[0]
html_list = html_list[index + 1:] html_list = html_list[index + 1:]
html_list[0] = html_tags + html_list[0] html_list[0] = html_tags + html_list[0]

View File

@ -29,16 +29,14 @@ import copy
from PyQt5 import QtCore, QtWebChannel, QtWidgets from PyQt5 import QtCore, QtWebChannel, QtWidgets
from openlp.core.common.path import Path, path_to_str from openlp.core.common.path import path_to_str
from openlp.core.common.settings import Settings from openlp.core.common.settings import Settings
from openlp.core.common.registry import Registry from openlp.core.common.registry import Registry
from openlp.core.common.applocation import AppLocation
from openlp.core.ui import HideMode from openlp.core.ui import HideMode
from openlp.core.display.screens import ScreenList from openlp.core.display.screens import ScreenList
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
DISPLAY_PATH = Path(__file__).parent / 'html' / 'display.html'
CHECKERBOARD_PATH = Path(__file__).parent / 'html' / 'checkerboard.png'
OPENLP_SPLASH_SCREEN_PATH = Path(__file__).parent / 'html' / 'openlp-splash-screen.png'
class MediaWatcher(QtCore.QObject): class MediaWatcher(QtCore.QObject):
@ -126,7 +124,11 @@ class DisplayWindow(QtWidgets.QWidget):
self.webview.page().setBackgroundColor(QtCore.Qt.transparent) self.webview.page().setBackgroundColor(QtCore.Qt.transparent)
self.layout.addWidget(self.webview) self.layout.addWidget(self.webview)
self.webview.loadFinished.connect(self.after_loaded) self.webview.loadFinished.connect(self.after_loaded)
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(DISPLAY_PATH))) display_base_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'display' / 'html'
self.display_path = display_base_path / 'display.html'
self.checkerboard_path = display_base_path / 'checkerboard.png'
self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png'
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
self.media_watcher = MediaWatcher(self) self.media_watcher = MediaWatcher(self)
self.channel = QtWebChannel.QWebChannel(self) self.channel = QtWebChannel.QWebChannel(self)
self.channel.registerObject('mediaWatcher', self.media_watcher) self.channel.registerObject('mediaWatcher', self.media_watcher)
@ -169,7 +171,7 @@ class DisplayWindow(QtWidgets.QWidget):
bg_color = Settings().value('core/logo background color') bg_color = Settings().value('core/logo background color')
image = Settings().value('core/logo file') image = Settings().value('core/logo file')
if path_to_str(image).startswith(':'): if path_to_str(image).startswith(':'):
image = OPENLP_SPLASH_SCREEN_PATH image = self.openlp_splash_screen_path
image_uri = image.as_uri() image_uri = image.as_uri()
self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color, self.run_javascript('Display.setStartupSplashScreen("{bg_color}", "{image}");'.format(bg_color=bg_color,
image=image_uri)) image=image_uri))
@ -329,7 +331,7 @@ class DisplayWindow(QtWidgets.QWidget):
if theme.background_type == 'transparent' and not self.is_display: if theme.background_type == 'transparent' and not self.is_display:
theme_copy = copy.deepcopy(theme) theme_copy = copy.deepcopy(theme)
theme_copy.background_type = 'image' theme_copy.background_type = 'image'
theme_copy.background_filename = CHECKERBOARD_PATH theme_copy.background_filename = self.checkerboard_path
exported_theme = theme_copy.export_theme() exported_theme = theme_copy.export_theme()
else: else:
exported_theme = theme.export_theme() exported_theme = theme.export_theme()

View File

@ -417,11 +417,17 @@ class ProjectorDB(Manager):
value: (str) From ProjectorSource, Sources tables or PJLink default code list value: (str) From ProjectorSource, Sources tables or PJLink default code list
""" """
source_dict = {} source_dict = {}
# Apparently, there was a change to the projector object. Test for which object has db id
if hasattr(projector, "id"):
chk = projector.id
elif hasattr(projector.entry, "id"):
chk = projector.entry.id
# Get default list first # Get default list first
for key in projector.source_available: for key in projector.source_available:
item = self.get_object_filtered(ProjectorSource, item = self.get_object_filtered(ProjectorSource,
and_(ProjectorSource.code == key, and_(ProjectorSource.code == key,
ProjectorSource.projector_id == projector.id)) ProjectorSource.projector_id == chk))
if item is None: if item is None:
source_dict[key] = PJLINK_DEFAULT_CODES[key] source_dict[key] = PJLINK_DEFAULT_CODES[key]
else: else:

View File

@ -46,31 +46,10 @@ from openlp.core.projectors.sourceselectform import SourceSelectSingle, SourceSe
from openlp.core.ui.icons import UiIcons from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.toolbar import OpenLPToolbar from openlp.core.widgets.toolbar import OpenLPToolbar
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug('projectormanager loaded') 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_SOCKET_TIMEOUT: ':/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 UiProjectorManager(object): class UiProjectorManager(object):
""" """
UI part of the Projector Manager UI part of the Projector Manager
@ -122,7 +101,7 @@ class UiProjectorManager(object):
self.one_toolbar.add_toolbar_action('connect_projector', self.one_toolbar.add_toolbar_action('connect_projector',
text=translate('OpenLP.ProjectorManager', text=translate('OpenLP.ProjectorManager',
'Connect to selected projector.'), 'Connect to selected projector.'),
icon=UiIcons().projector_connect, icon=UiIcons().projector_select_connect,
tooltip=translate('OpenLP.ProjectorManager', tooltip=translate('OpenLP.ProjectorManager',
'Connect to selected projector.'), 'Connect to selected projector.'),
triggers=self.on_connect_projector) triggers=self.on_connect_projector)
@ -136,7 +115,7 @@ class UiProjectorManager(object):
self.one_toolbar.add_toolbar_action('disconnect_projector', self.one_toolbar.add_toolbar_action('disconnect_projector',
text=translate('OpenLP.ProjectorManager', text=translate('OpenLP.ProjectorManager',
'Disconnect from selected projectors'), 'Disconnect from selected projectors'),
icon=UiIcons().projector_disconnect, icon=UiIcons().projector_select_disconnect,
tooltip=translate('OpenLP.ProjectorManager', tooltip=translate('OpenLP.ProjectorManager',
'Disconnect from selected projector.'), 'Disconnect from selected projector.'),
triggers=self.on_disconnect_projector) triggers=self.on_disconnect_projector)
@ -151,7 +130,7 @@ class UiProjectorManager(object):
self.one_toolbar.add_toolbar_action('poweron_projector', self.one_toolbar.add_toolbar_action('poweron_projector',
text=translate('OpenLP.ProjectorManager', text=translate('OpenLP.ProjectorManager',
'Power on selected projector'), 'Power on selected projector'),
icon=UiIcons().projector_on, icon=UiIcons().projector_power_on,
tooltip=translate('OpenLP.ProjectorManager', tooltip=translate('OpenLP.ProjectorManager',
'Power on selected projector.'), 'Power on selected projector.'),
triggers=self.on_poweron_projector) triggers=self.on_poweron_projector)
@ -164,7 +143,7 @@ class UiProjectorManager(object):
triggers=self.on_poweron_projector) triggers=self.on_poweron_projector)
self.one_toolbar.add_toolbar_action('poweroff_projector', self.one_toolbar.add_toolbar_action('poweroff_projector',
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
icon=UiIcons().projector_off, icon=UiIcons().projector_power_off,
tooltip=translate('OpenLP.ProjectorManager', tooltip=translate('OpenLP.ProjectorManager',
'Put selected projector in standby.'), 'Put selected projector in standby.'),
triggers=self.on_poweroff_projector) triggers=self.on_poweroff_projector)
@ -309,7 +288,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
S_INITIALIZE: UiIcons().projector_on, S_INITIALIZE: UiIcons().projector_on,
S_STANDBY: UiIcons().projector_off, S_STANDBY: UiIcons().projector_off,
S_WARMUP: UiIcons().projector_warmup, S_WARMUP: UiIcons().projector_warmup,
S_ON: UiIcons().projector_off, S_ON: UiIcons().projector_on,
S_COOLDOWN: UiIcons().projector_cooldown, S_COOLDOWN: UiIcons().projector_cooldown,
E_ERROR: UiIcons().projector_error, E_ERROR: UiIcons().projector_error,
E_NETWORK: UiIcons().error, E_NETWORK: UiIcons().error,
@ -880,6 +859,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
""" """
Update the icons when the selected projectors change Update the icons when the selected projectors change
""" """
log.debug('update_icons(): Checking for selected projector items in list')
count = len(self.projector_list_widget.selectedItems()) count = len(self.projector_list_widget.selectedItems())
projector = None projector = None
if count == 0: if count == 0:
@ -900,6 +880,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
self.get_toolbar_item('blank_projector_multiple', hidden=True) self.get_toolbar_item('blank_projector_multiple', hidden=True)
self.get_toolbar_item('show_projector_multiple', hidden=True) self.get_toolbar_item('show_projector_multiple', hidden=True)
elif count == 1: elif count == 1:
log.debug('update_icons(): Found one item selected')
projector = self.projector_list_widget.selectedItems()[0].data(QtCore.Qt.UserRole) projector = self.projector_list_widget.selectedItems()[0].data(QtCore.Qt.UserRole)
connected = QSOCKET_STATE[projector.link.state()] == S_CONNECTED connected = QSOCKET_STATE[projector.link.state()] == S_CONNECTED
power = projector.link.power == S_ON power = projector.link.power == S_ON
@ -910,12 +891,14 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
self.get_toolbar_item('blank_projector_multiple', hidden=True) self.get_toolbar_item('blank_projector_multiple', hidden=True)
self.get_toolbar_item('show_projector_multiple', hidden=True) self.get_toolbar_item('show_projector_multiple', hidden=True)
if connected: if connected:
log.debug('update_icons(): Updating icons for connected state')
self.get_toolbar_item('view_projector', enabled=True) self.get_toolbar_item('view_projector', enabled=True)
self.get_toolbar_item('source_view_projector', self.get_toolbar_item('source_view_projector',
enabled=connected and power and projector.link.source_available is not None) enabled=projector.link.source_available is not None and connected and power)
self.get_toolbar_item('edit_projector', hidden=True) self.get_toolbar_item('edit_projector', hidden=True)
self.get_toolbar_item('delete_projector', hidden=True) self.get_toolbar_item('delete_projector', hidden=True)
else: else:
log.debug('update_icons(): Updating for not connected state')
self.get_toolbar_item('view_projector', hidden=True) self.get_toolbar_item('view_projector', hidden=True)
self.get_toolbar_item('source_view_projector', hidden=True) self.get_toolbar_item('source_view_projector', hidden=True)
self.get_toolbar_item('edit_projector', enabled=True) self.get_toolbar_item('edit_projector', enabled=True)
@ -931,6 +914,7 @@ class ProjectorManager(QtWidgets.QWidget, RegistryBase, UiProjectorManager, LogM
self.get_toolbar_item('blank_projector', enabled=False) self.get_toolbar_item('blank_projector', enabled=False)
self.get_toolbar_item('show_projector', enabled=False) self.get_toolbar_item('show_projector', enabled=False)
else: else:
log.debug('update_icons(): Updating for multiple items selected')
self.get_toolbar_item('edit_projector', enabled=False) self.get_toolbar_item('edit_projector', enabled=False)
self.get_toolbar_item('delete_projector', enabled=False) self.get_toolbar_item('delete_projector', enabled=False)
self.get_toolbar_item('view_projector', hidden=True) self.get_toolbar_item('view_projector', hidden=True)

View File

@ -528,8 +528,9 @@ class PJLinkCommands(object):
sources.append(source) sources.append(source)
sources.sort() sources.sort()
self.source_available = sources self.source_available = sources
log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.entry.name, log.debug('({ip}) Setting projector source_available to "{data}"'.format(ip=self.entry.name,
data=self.source_available)) data=self.source_available))
self.projectorUpdateIcons.emit()
return return
def process_lamp(self, data): def process_lamp(self, data):
@ -886,6 +887,9 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
elif status >= S_NOT_CONNECTED and status in QSOCKET_STATE: elif status >= S_NOT_CONNECTED and status in QSOCKET_STATE:
# Socket connection status update # Socket connection status update
self.status_connect = status self.status_connect = status
# Check if we need to update error state as well
if self.error_status != S_OK and status != S_NOT_CONNECTED:
self.error_status = S_OK
elif status >= S_NOT_CONNECTED and status in PROJECTOR_STATE: elif status >= S_NOT_CONNECTED and status in PROJECTOR_STATE:
# Only affects the projector status # Only affects the projector status
self.projector_status = status self.projector_status = status
@ -905,7 +909,14 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
message=status_message if msg is None else msg)) message=status_message if msg is None else msg))
# Now that we logged extra information for debugging, broadcast the original change/message # Now that we logged extra information for debugging, broadcast the original change/message
(code, message) = self._get_status(status) # Check for connection errors first
if self.error_status != S_OK:
log.debug('({ip}) Signalling error code'.format(ip=self.entry.name))
code, message = self._get_status(self.error_status)
status = self.error_status
else:
log.debug('({ip}) Signalling status code'.format(ip=self.entry.name))
code, message = self._get_status(status)
if msg is not None: if msg is not None:
message = msg message = msg
elif message is None: elif message is None:
@ -953,7 +964,7 @@ class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.entry.name)) log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.entry.name))
return self.disconnect_from_host() return self.disconnect_from_host()
# Convert the initial login prompt with the expected PJLink normal command format for processing # Convert the initial login prompt with the expected PJLink normal command format for processing
log.debug('({ip}) check_login(): Formatting initial connection prompt' log.debug('({ip}) check_login(): Formatting initial connection prompt '
'to PJLink packet'.format(ip=self.entry.name)) 'to PJLink packet'.format(ip=self.entry.name))
return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX, return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX,
clss='1', clss='1',

View File

@ -455,7 +455,7 @@ class AdvancedTab(SettingsTab):
Service Name options changed Service Name options changed
""" """
self.service_name_day.setEnabled(default_service_enabled) self.service_name_day.setEnabled(default_service_enabled)
time_enabled = default_service_enabled and self.service_name_day.currentIndex() is not 7 time_enabled = default_service_enabled and self.service_name_day.currentIndex() != 7
self.service_name_time.setEnabled(time_enabled) self.service_name_time.setEnabled(time_enabled)
self.service_name_edit.setEnabled(default_service_enabled) self.service_name_edit.setEnabled(default_service_enabled)
self.service_name_revert_button.setEnabled(default_service_enabled) self.service_name_revert_button.setEnabled(default_service_enabled)
@ -497,7 +497,7 @@ class AdvancedTab(SettingsTab):
""" """
React to the day of the service name changing. React to the day of the service name changing.
""" """
self.service_name_time.setEnabled(service_day is not 7) self.service_name_time.setEnabled(service_day != 7)
self.update_service_name_example(None) self.update_service_name_example(None)
def on_service_name_revert_button_clicked(self): def on_service_name_revert_button_clicked(self):

View File

@ -117,13 +117,17 @@ class UiIcons(object):
'presentation': {'icon': 'fa.bar-chart'}, 'presentation': {'icon': 'fa.bar-chart'},
'preview': {'icon': 'fa.laptop'}, 'preview': {'icon': 'fa.laptop'},
'projector': {'icon': 'op.video'}, 'projector': {'icon': 'op.video'},
'projector_connect': {'icon': 'fa.plug'}, 'projector_connect': {'icon': 'fa.plug'}, # Projector connect
'projector_cooldown': {'icon': 'fa.video-camera', 'attr': 'blue'}, 'projector_cooldown': {'icon': 'fa.video-camera', 'attr': 'blue'},
'projector_disconnect': {'icon': 'fa.plug', 'attr': 'lightGray'}, 'projector_disconnect': {'icon': 'fa.plug', 'attr': 'lightGray'}, # Projector disconnect
'projector_error': {'icon': 'fa.video-camera', 'attr': 'red'}, 'projector_error': {'icon': 'fa.video-camera', 'attr': 'red'},
'projector_hdmi': {'icon': 'op.hdmi'}, 'projector_hdmi': {'icon': 'op.hdmi'},
'projector_off': {'icon': 'fa.video-camera', 'attr': 'black'}, 'projector_power_off': {'icon': 'fa.video-camera', 'attr': 'red'}, # Toolbar power off
'projector_on': {'icon': 'fa.video-camera', 'attr': 'green'}, 'projector_power_on': {'icon': 'fa.video-camera', 'attr': 'green'}, # Toolbar power on
'projector_off': {'icon': 'fa.video-camera', 'attr': 'black'}, # Projector off
'projector_on': {'icon': 'fa.video-camera', 'attr': 'green'}, # Projector on
'projector_select_connect': {'icon': 'fa.plug', 'attr': 'green'}, # Toolbar connect
'projector_select_disconnect': {'icon': 'fa.plug', 'attr': 'red'}, # Toolbar disconnect
'projector_warmup': {'icon': 'fa.video-camera', 'attr': 'yellow'}, 'projector_warmup': {'icon': 'fa.video-camera', 'attr': 'yellow'},
'picture': {'icon': 'fa.picture-o'}, 'picture': {'icon': 'fa.picture-o'},
'print': {'icon': 'fa.print'}, 'print': {'icon': 'fa.print'},

View File

@ -206,7 +206,7 @@ class ThemesTab(SettingsTab):
find_and_set_in_combo_box(self.default_combo_box, self.global_theme) find_and_set_in_combo_box(self.default_combo_box, self.global_theme)
# self.renderer.set_global_theme() # self.renderer.set_global_theme()
self.renderer.set_theme_level(self.theme_level) self.renderer.set_theme_level(self.theme_level)
if self.global_theme is not '': if self.global_theme != '':
self._preview_global_theme() self._preview_global_theme()
def _preview_global_theme(self): def _preview_global_theme(self):

View File

@ -46,6 +46,7 @@ from openlp.plugins.presentations.lib.presentationcontroller import Presentation
if is_win(): if is_win():
from win32com.client import Dispatch from win32com.client import Dispatch
import pywintypes import pywintypes
uno_available = False
# Declare an empty exception to match the exception imported from UNO # Declare an empty exception to match the exception imported from UNO
class ErrorCodeIOException(Exception): class ErrorCodeIOException(Exception):

View File

@ -263,8 +263,9 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
self.login_progress_bar.setVisible(True) self.login_progress_bar.setVisible(True)
self.application.process_events() self.application.process_events()
# Log the user in # Log the user in
if not self.song_select_importer.login( subscription_level = self.song_select_importer.login(
self.username_edit.text(), self.password_edit.text(), self._update_login_progress): self.username_edit.text(), self.password_edit.text(), self._update_login_progress)
if not subscription_level:
QtWidgets.QMessageBox.critical( QtWidgets.QMessageBox.critical(
self, self,
translate('SongsPlugin.SongSelectForm', 'Error Logging In'), translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
@ -272,6 +273,14 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
'There was a problem logging in, perhaps your username or password is incorrect?') 'There was a problem logging in, perhaps your username or password is incorrect?')
) )
else: else:
if subscription_level == 'Free':
QtWidgets.QMessageBox.information(
self,
translate('SongsPlugin.SongSelectForm', 'Free user'),
translate('SongsPlugin.SongSelectForm', 'You logged in with a free account, '
'the search will be limited to songs '
'in the public domain.')
)
if self.save_password_checkbox.isChecked(): if self.save_password_checkbox.isChecked():
Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text()) Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text())
Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text()) Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text())

View File

@ -81,7 +81,7 @@ class SongSelectImport(object):
:param username: SongSelect username :param username: SongSelect username
:param password: SongSelect password :param password: SongSelect password
:param callback: Method to notify of progress. :param callback: Method to notify of progress.
:return: True on success, False on failure. :return: subscription level on success, None on failure.
""" """
if callback: if callback:
callback() callback()
@ -115,11 +115,31 @@ class SongSelectImport(object):
return False return False
if callback: if callback:
callback() callback()
# Page if user is in an organization
if posted_page.find('input', id='SearchText') is not None: if posted_page.find('input', id='SearchText') is not None:
return True self.subscription_level = self.find_subscription_level(posted_page)
return self.subscription_level
# Page if user is not in an organization
elif posted_page.find('div', id="select-organization") is not None:
try:
home_page = BeautifulSoup(self.opener.open(BASE_URL).read(), 'lxml')
except (TypeError, URLError) as error:
log.exception('Could not reach SongSelect, {error}'.format(error=error))
self.subscription_level = self.find_subscription_level(home_page)
return self.subscription_level
else: else:
log.debug(posted_page) log.debug(posted_page)
return False return None
def find_subscription_level(self, page):
scripts = page.find_all('script')
for tag in scripts:
if tag.string:
match = re.search("'Subscription': '(?P<subscription_level>[^']+)", tag.string)
if match:
return match.group('subscription_level')
log.error('Could not determine SongSelect subscription level')
return None
def logout(self): def logout(self):
""" """
@ -128,7 +148,7 @@ class SongSelectImport(object):
try: try:
self.opener.open(LOGOUT_URL) self.opener.open(LOGOUT_URL)
except (TypeError, URLError) as error: except (TypeError, URLError) as error:
log.exception('Could not log of SongSelect, {error}'.format(error=error)) log.exception('Could not log out of SongSelect, {error}'.format(error=error))
def search(self, search_text, max_results, callback=None): def search(self, search_text, max_results, callback=None):
""" """
@ -145,7 +165,7 @@ class SongSelectImport(object):
'PrimaryLanguage': '', 'PrimaryLanguage': '',
'Keys': '', 'Keys': '',
'Themes': '', 'Themes': '',
'List': '', 'List': 'publicdomain' if self.subscription_level == 'Free' else '',
'Sort': '', 'Sort': '',
'SearchText': search_text 'SearchText': search_text
} }

View File

@ -121,7 +121,7 @@ class SongsPlugin(Plugin):
self.song_export_item.setVisible(True) self.song_export_item.setVisible(True)
self.song_tools_menu.menuAction().setVisible(True) self.song_tools_menu.menuAction().setVisible(True)
action_list = ActionList.get_instance() action_list = ActionList.get_instance()
action_list.add_action(self.song_import_item, UiStrings().Import) #action_list.add_action(self.song_import_item, UiStrings().Import)
action_list.add_action(self.song_export_item, UiStrings().Export) action_list.add_action(self.song_export_item, UiStrings().Export)
action_list.add_action(self.tools_reindex_item, UiStrings().Tools) action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
action_list.add_action(self.tools_find_duplicates, UiStrings().Tools) action_list.add_action(self.tools_find_duplicates, UiStrings().Tools)

View File

@ -1,5 +1,8 @@
version: OpenLP-win-ci-b{build} version: OpenLP-win-ci-b{build}
image:
- Visual Studio 2017
clone_script: clone_script:
- curl -L https://bazaar.launchpad.net/BRANCHPATH/tarball -o sourcecode.tar.gz - curl -L https://bazaar.launchpad.net/BRANCHPATH/tarball -o sourcecode.tar.gz
- 7z e sourcecode.tar.gz - 7z e sourcecode.tar.gz
@ -7,15 +10,17 @@ clone_script:
- mv BRANCHPATH openlp-branch - mv BRANCHPATH openlp-branch
environment: environment:
PYTHON: C:\\Python37-x64 matrix:
- PYTHON: C:\\Python37-x64
- PYTHON: C:\\Python37
install: install:
# Install dependencies from pypi # Install dependencies from pypi
- "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python nose mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 pymediainfo" - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic appdirs chardet beautifulsoup4 lxml Mako mysql-connector-python pytest mock pyodbc psycopg2 pypiwin32 websockets asyncio waitress six webob requests QtAwesome PyQt5 PyQtWebEngine pymediainfo"
# Download and unpack mupdf # Download and unpack mupdf
- appveyor DownloadFile https://mupdf.com/downloads/archive/mupdf-1.14.0-windows.zip - appveyor DownloadFile https://mupdf.com/downloads/archive/mupdf-1.14.0-windows.zip
- 7z x mupdf-1.14.0-windows.zip - 7z x mupdf-1.14.0-windows.zip
- cp mupdf-1.14.0-windows/mupdf.exe openlp-branch/mupdf.exe - cp mupdf-1.14.0-windows/mutool.exe openlp-branch/mutool.exe
# Download and unpack mediainfo # Download and unpack mediainfo
- appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/18.08.1/MediaInfo_CLI_18.08.1_Windows_i386.zip - appveyor DownloadFile https://mediaarea.net/download/binary/mediainfo/18.08.1/MediaInfo_CLI_18.08.1_Windows_i386.zip
- mkdir MediaInfo - mkdir MediaInfo
@ -27,7 +32,7 @@ build: off
test_script: test_script:
- cd openlp-branch - cd openlp-branch
# Run the tests # Run the tests
- "%PYTHON%\\python.exe -m nose -v tests" - "%PYTHON%\\python.exe -m pytest -v tests"
# Go back to the user root folder # Go back to the user root folder
- cd.. - cd..

View File

@ -9,6 +9,7 @@ ignore = E402,E722,W503,W504
[flake8] [flake8]
exclude=resources.py,vlc.py exclude=resources.py,vlc.py
max-line-length = 120
ignore = E402,W503,W504,D ignore = E402,W503,W504,D
[pycodestyle] [pycodestyle]

View File

@ -15,13 +15,12 @@
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. # # more details. #
# # # #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
from tempfile import mkdtemp
# You should have received a copy of the GNU General Public License along # # 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 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import os
from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -57,7 +56,8 @@ class TestRemoteDeploy(TestCase):
# GIVEN: A new downloaded zip file # GIVEN: A new downloaded zip file
mocked_zipfile = MagicMock() mocked_zipfile = MagicMock()
MockZipFile.return_value = mocked_zipfile MockZipFile.return_value = mocked_zipfile
root_path = Path('/tmp/remotes') root_path_str = '{sep}tmp{sep}remotes'.format(sep=os.sep)
root_path = Path(root_path_str)
# WHEN: deploy_zipfile() is called # WHEN: deploy_zipfile() is called
deploy_zipfile(root_path, 'site.zip') deploy_zipfile(root_path, 'site.zip')

View File

@ -322,7 +322,7 @@ class TestPath(TestCase):
obj = path.json_object(extra=1, args=2) obj = path.json_object(extra=1, args=2)
# THEN: A JSON decodable object should have been returned. # THEN: A JSON decodable object should have been returned.
assert obj == {'__Path__': ('/', 'base', 'path', 'to', 'fi.le')} assert obj == {'__Path__': (os.sep, 'base', 'path', 'to', 'fi.le')}
def test_path_json_object_base_path(self): def test_path_json_object_base_path(self):
""" """

View File

@ -178,7 +178,7 @@ class TestImageManager(TestCase, TestMixin):
# THEN a KeyError is thrown # THEN a KeyError is thrown
with self.assertRaises(KeyError) as context: with self.assertRaises(KeyError) as context:
self.image_manager.get_image(TEST_PATH, 'church1.jpg') self.image_manager.get_image(TEST_PATH, 'church1.jpg')
assert context.exception is not '', 'KeyError exception should have been thrown for missing image' assert context.exception != '', 'KeyError exception should have been thrown for missing image'
@patch('openlp.core.lib.imagemanager.run_thread') @patch('openlp.core.lib.imagemanager.run_thread')
def test_different_dimension_image(self, mocked_run_thread): def test_different_dimension_image(self, mocked_run_thread):
@ -211,7 +211,7 @@ class TestImageManager(TestCase, TestMixin):
# WHEN: calling with correct image, but wrong dimensions # WHEN: calling with correct image, but wrong dimensions
with self.assertRaises(KeyError) as context: with self.assertRaises(KeyError) as context:
self.image_manager.get_image(full_path, 'church.jpg', 120, 120) self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
assert context.exception is not '', 'KeyError exception should have been thrown for missing dimension' assert context.exception != '', 'KeyError exception should have been thrown for missing dimension'
@patch('openlp.core.lib.imagemanager.resize_image') @patch('openlp.core.lib.imagemanager.resize_image')
@patch('openlp.core.lib.imagemanager.image_to_byte') @patch('openlp.core.lib.imagemanager.image_to_byte')

View File

@ -154,13 +154,13 @@ class TestServiceItem(TestCase, TestMixin):
mocked_get_section_data_path: mocked_get_section_data_path:
mocked_exists.return_value = True mocked_exists.return_value = True
mocked_get_section_data_path.return_value = Path('/path/') mocked_get_section_data_path.return_value = Path('/path/')
service_item.set_from_service(line, TEST_PATHb) service_item.set_from_service(line, TEST_PATH)
# THEN: We should get back a valid service item # THEN: We should get back a valid service item
assert service_item.is_valid is True, 'The new service item should be valid' assert service_item.is_valid is True, 'The new service item should be valid'
assert test_file == service_item.get_rendered_frame(0), 'The first frame should match the path to the image' assert test_file == service_item.get_rendered_frame(0), 'The first frame should match the path to the image'
assert frame_array == service_item.get_frames()[0], 'The return should match frame array1' assert frame_array == service_item.get_frames()[0], 'The return should match frame array1'
assert test_file == str(service_item.get_frame_path(0)), \ assert test_file == service_item.get_frame_path(0), \
'The frame path should match the full path to the image' 'The frame path should match the full path to the image'
assert image_name == service_item.get_frame_title(0), 'The frame title should match the image name' assert image_name == service_item.get_frame_title(0), 'The frame title should match the image name'
assert image_name == service_item.get_display_title(), 'The display title should match the first image name' assert image_name == service_item.get_display_title(), 'The display title should match the first image name'
@ -328,7 +328,7 @@ class TestServiceItem(TestCase, TestMixin):
# WHEN: We add a custom from a saved service # WHEN: We add a custom from a saved service
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj') line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
service_item.set_from_service(line, '/test/') service_item.set_from_service(line, Path('/test/'))
# THEN: We should get back a valid service item # THEN: We should get back a valid service item
assert service_item.is_valid is True, 'The new service item should be valid' assert service_item.is_valid is True, 'The new service item should be valid'

View File

@ -57,4 +57,4 @@ class MediaPluginTest(TestCase, TestMixin):
# THEN: about() should return a string object # THEN: about() should return a string object
assert isinstance(MediaPlugin.about(), str) assert isinstance(MediaPlugin.about(), str)
# THEN: about() should return a non-empty string # THEN: about() should return a non-empty string
assert len(MediaPlugin.about()) is not 0 assert len(MediaPlugin.about()) != 0

View File

@ -199,7 +199,7 @@ class TestLib(TestCase):
result = _remove_typos(diff) result = _remove_typos(diff)
# THEN: There should be no typos in the middle anymore. The remaining equals should have been merged. # THEN: There should be no typos in the middle anymore. The remaining equals should have been merged.
assert len(result) is 1, 'The result should contain only one element.' assert len(result) == 1, 'The result should contain only one element.'
assert result[0][0] == 'equal', 'The result should contain an equal element.' assert result[0][0] == 'equal', 'The result should contain an equal element.'
assert result[0][1] == 0, 'The start indices should be kept.' assert result[0][1] == 0, 'The start indices should be kept.'
assert result[0][2] == 22, 'The stop indices should be kept.' assert result[0][2] == 22, 'The stop indices should be kept.'

View File

@ -64,7 +64,7 @@ class TestSongSelectImport(TestCase, TestMixin):
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener): def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener):
""" """
Test that when logging in to SongSelect fails, the login method returns False Test that when logging in to SongSelect fails, the login method returns None
""" """
# GIVEN: A bunch of mocked out stuff and an importer object # GIVEN: A bunch of mocked out stuff and an importer object
mocked_opener = MagicMock() mocked_opener = MagicMock()
@ -80,12 +80,12 @@ class TestSongSelectImport(TestCase, TestMixin):
# WHEN: The login method is called after being rigged to fail # WHEN: The login method is called after being rigged to fail
result = importer.login('username', 'password', mock_callback) result = importer.login('username', 'password', mock_callback)
# THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned # THEN: callback was called 3 times, open was called twice, find was called twice, and None was returned
assert 3 == mock_callback.call_count, 'callback should have been called 3 times' assert 3 == mock_callback.call_count, 'callback should have been called 3 times'
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice' assert 2 == mocked_login_page.find.call_count, 'find should have been called twice'
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once' assert 2 == mocked_posted_page.find.call_count, 'find should have been called twice'
assert 2 == mocked_opener.open.call_count, 'opener should have been called twice' assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
assert result is False, 'The login method should have returned False' assert result is None, 'The login method should have returned None'
@patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.build_opener')
def test_login_except(self, mocked_build_opener): def test_login_except(self, mocked_build_opener):
@ -129,7 +129,7 @@ class TestSongSelectImport(TestCase, TestMixin):
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page' assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page' assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
assert 2 == mocked_opener.open.call_count, 'opener should have been called twice' assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
assert result is True, 'The login method should have returned True' assert result is None, 'The login method should have returned the subscription level'
@patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.build_opener')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup') @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
@ -146,7 +146,8 @@ class TestSongSelectImport(TestCase, TestMixin):
mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form] mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form]
mocked_posted_page = MagicMock() mocked_posted_page = MagicMock()
mocked_posted_page.find.return_value = MagicMock() mocked_posted_page.find.return_value = MagicMock()
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page] mocked_home_page = MagicMock()
MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page, mocked_home_page]
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
@ -158,7 +159,7 @@ class TestSongSelectImport(TestCase, TestMixin):
assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page' assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page' assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
assert 'https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0] assert 'https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0]
assert result is True, 'The login method should have returned True' assert result is None, 'The login method should have returned the subscription level'
@patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.build_opener')
def test_logout(self, mocked_build_opener): def test_logout(self, mocked_build_opener):
@ -191,6 +192,7 @@ class TestSongSelectImport(TestCase, TestMixin):
MockedBeautifulSoup.return_value = mocked_results_page MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
importer.subscription_level = 'premium'
# WHEN: The login method is called after being rigged to fail # WHEN: The login method is called after being rigged to fail
results = importer.search('text', 1000, mock_callback) results = importer.search('text', 1000, mock_callback)
@ -231,6 +233,7 @@ class TestSongSelectImport(TestCase, TestMixin):
MockedBeautifulSoup.return_value = mocked_results_page MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
importer.subscription_level = 'premium'
# WHEN: The search method is called # WHEN: The search method is called
results = importer.search('text', 1000, mock_callback) results = importer.search('text', 1000, mock_callback)
@ -282,6 +285,7 @@ class TestSongSelectImport(TestCase, TestMixin):
MockedBeautifulSoup.return_value = mocked_results_page MockedBeautifulSoup.return_value = mocked_results_page
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
importer.subscription_level = 'premium'
# WHEN: The search method is called # WHEN: The search method is called
results = importer.search('text', 2, mock_callback) results = importer.search('text', 2, mock_callback)

View File

@ -45,8 +45,8 @@ class TestSongUsage(TestCase):
# THEN: about() should return a string object # THEN: about() should return a string object
assert isinstance(SongUsagePlugin.about(), str) assert isinstance(SongUsagePlugin.about(), str)
# THEN: about() should return a non-empty string # THEN: about() should return a non-empty string
assert len(SongUsagePlugin.about()) is not 0 assert len(SongUsagePlugin.about()) != 0
assert len(SongUsagePlugin.about()) is not 0 assert len(SongUsagePlugin.about()) != 0
@patch('openlp.plugins.songusage.songusageplugin.Manager') @patch('openlp.plugins.songusage.songusageplugin.Manager')
def test_song_usage_init(self, MockedManager): def test_song_usage_init(self, MockedManager):

View File

@ -134,6 +134,7 @@ class TestProjectorDB(TestCase, TestMixin):
""" """
Set up anything necessary for all tests Set up anything necessary for all tests
""" """
self.tmp_folder = mkdtemp(prefix='openlp_')
# Create a test app to keep from segfaulting # Create a test app to keep from segfaulting
Registry.create() Registry.create()
self.registry = Registry() self.registry = Registry()
@ -153,11 +154,11 @@ class TestProjectorDB(TestCase, TestMixin):
patch('openlp.core.ui.mainwindow.ThemeManager'), \ patch('openlp.core.ui.mainwindow.ThemeManager'), \
patch('openlp.core.ui.mainwindow.ProjectorManager'), \ patch('openlp.core.ui.mainwindow.ProjectorManager'), \
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \ patch('openlp.core.ui.mainwindow.websockets.WebSocketServer'), \
patch('openlp.core.ui.mainwindow.server.HttpServer'): patch('openlp.core.ui.mainwindow.server.HttpServer'), \
patch('openlp.core.state.State.list_plugins') as mock_plugins:
mock_plugins.return_value = []
self.main_window = MainWindow() self.main_window = MainWindow()
# Create a temporary database directory and database
self.tmp_folder = mkdtemp(prefix='openlp_')
tmpdb_url = 'sqlite:///{db}'.format(db=os.path.join(self.tmp_folder, TEST_DB)) tmpdb_url = 'sqlite:///{db}'.format(db=os.path.join(self.tmp_folder, TEST_DB))
mocked_init_url.return_value = tmpdb_url mocked_init_url.return_value = tmpdb_url
self.projector = ProjectorDB() self.projector = ProjectorDB()

View File

@ -452,12 +452,16 @@ class TestPJLinkCommands(TestCase):
""" """
Test saving video source available information Test saving video source available information
""" """
# GIVEN: Test object # GIVEN: Test object
with patch.object(openlp.core.projectors.pjlink, 'log') as mock_log: with patch.object(openlp.core.projectors.pjlink, 'log') as mock_log:
pjlink = PJLink(Projector(**TEST1_DATA), no_poll=True) pjlink = PJLink(Projector(**TEST1_DATA), no_poll=True)
pjlink.source_available = [] pjlink.source_available = []
log_debug_calls = [call('({ip}) Setting projector sources_available to ' log_debug_calls = [call('({ip}) reset_information() connect status is '
'S_NOT_CONNECTED'.format(ip=pjlink.name)),
call('({ip}) Setting projector source_available to '
'"[\'11\', \'12\', \'21\', \'22\', \'31\', \'32\']"'.format(ip=pjlink.name))] '"[\'11\', \'12\', \'21\', \'22\', \'31\', \'32\']"'.format(ip=pjlink.name))]
chk_data = '21 12 11 22 32 31' # Although they should already be sorted, use unsorted to verify method chk_data = '21 12 11 22 32 31' # Although they should already be sorted, use unsorted to verify method
chk_test = ['11', '12', '21', '22', '31', '32'] chk_test = ['11', '12', '21', '22', '31', '32']

View File

@ -24,7 +24,7 @@ Package to test for proper bzr tags.
""" """
import os import os
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from unittest import TestCase from unittest import TestCase, SkipTest
TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10', TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10',
@ -42,7 +42,10 @@ class TestBzrTags(TestCase):
path = os.path.dirname(__file__) path = os.path.dirname(__file__)
# WHEN getting the branches tags # WHEN getting the branches tags
bzr = Popen(('bzr', 'tags', '--directory=' + path), stdout=PIPE) try:
bzr = Popen(('bzr', 'tags', '--directory=' + path), stdout=PIPE)
except Exception:
raise SkipTest('bzr is not installed')
std_out = bzr.communicate()[0] std_out = bzr.communicate()[0]
count = len(TAGS1) count = len(TAGS1)
tags = [line.decode('utf-8').split()[0] for line in std_out.splitlines()] tags = [line.decode('utf-8').split()[0] for line in std_out.splitlines()]