diff --git a/appveyor.yml b/appveyor.yml index 84d21d041..5ab2be0ad 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,11 +13,13 @@ environment: CHOCO_VLC_ARG: FORCE_PACKAGING: 0 FORCE_PACKAGING_MANUAL: 0 + PYICU_PACK: PyICU-2.9-cp38-cp38-win_amd64.whl - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 PY_DIR: C:\\Python38 CHOCO_VLC_ARG: --forcex86 FORCE_PACKAGING: 0 FORCE_PACKAGING_MANUAL: 0 + PYICU_PACK: PyICU-2.9-cp38-cp38-win32.whl - APPVEYOR_BUILD_WORKER_IMAGE: macos-catalina QT_QPA_PLATFORM: offscreen FORCE_PACKAGING: 0 @@ -36,9 +38,13 @@ install: # Install Windows only dependencies - cmd: python -m pip install pyodbc pypiwin32 - cmd: choco install vlc %CHOCO_VLC_ARG% --no-progress --limit-output + # Download and install pyicu for windows (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) + - cmd: python -m pip install https://get.openlp.org/win-sdk/%PYICU_PACK% # Mac only dependencies - - sh: python -m pip install Pyro4 'pyobjc-core<8.2' 'pyobjc-framework-Cocoa<8.2' py-applescript - sh: brew install --cask vlc + - sh: brew install pkg-config icu4c + - sh: PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/local/opt/icu4c/lib/pkgconfig" python -m pip install pyicu + - sh: python -m pip install Pyro4 'pyobjc-core<8.2' 'pyobjc-framework-Cocoa<8.2' py-applescript build: off diff --git a/openlp/.version b/openlp/.version index 4a36342fc..ae32d75e3 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -3.0.0 +3.0.0.dev6+dbea8bf56 \ No newline at end of file diff --git a/openlp/core/api/versions/v2/__init__.py b/openlp/core/api/versions/v2/__init__.py index 16b1fca57..e8e8d8d9f 100644 --- a/openlp/core/api/versions/v2/__init__.py +++ b/openlp/core/api/versions/v2/__init__.py @@ -22,7 +22,7 @@ from openlp.core.api.versions.v2.controller import controller_views from openlp.core.api.versions.v2.core import core from openlp.core.api.versions.v2.service import service_views -from openlp.core.api.versions.v2.plugins import plugins +from openlp.core.api.versions.v2.plugins import plugins, alert_1_views, alert_2_views def register_blueprints(app): @@ -30,3 +30,5 @@ def register_blueprints(app): app.register_blueprint(core, url_prefix='/api/v2/core/') app.register_blueprint(service_views, url_prefix='/api/v2/service/') app.register_blueprint(plugins, url_prefix='/api/v2/plugins/') + app.register_blueprint(alert_2_views, url_prefix='/api/v2/plugins/alerts') + app.register_blueprint(alert_1_views, url_prefix='/api/alert') diff --git a/openlp/core/api/versions/v2/plugins.py b/openlp/core/api/versions/v2/plugins.py index f1d050c0e..05a09f720 100644 --- a/openlp/core/api/versions/v2/plugins.py +++ b/openlp/core/api/versions/v2/plugins.py @@ -24,7 +24,7 @@ import logging import re from flask import abort, request, Blueprint, jsonify -from openlp.core.api.lib import login_required +from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth from openlp.core.lib.plugin import PluginStatus from openlp.core.common.registry import Registry from openlp.plugins.songs.lib import transpose_lyrics @@ -33,6 +33,8 @@ log = logging.getLogger(__name__) plugins = Blueprint('v2-plugins', __name__) +alert_1_views = Blueprint('v1-alert-plugin', __name__) +alert_2_views = Blueprint('v2-alert-plugin', __name__) def search(plugin_name, text): @@ -172,3 +174,27 @@ def transpose(transpose_value): chord_slides.append({'chords': verse_list[i + 1].strip(), 'verse': verse_list[i]}) return jsonify(chord_slides), 200 abort(400) + + +@alert_2_views.route('', methods=['POST']) +@login_required +def alert(): + data = request.json + if not data: + abort(400) + alert = data.get('text', '') + if alert: + if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: + Registry().get('alerts_manager').alerts_text.emit([alert]) + return '', 204 + abort(400) + + +@alert_1_views.route('') +@old_auth +def old_alert(): + alert = extract_request(request.args.get('data', ''), 'text') + if alert: + if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: + Registry().get('alerts_manager').alerts_text.emit([alert]) + return old_success_response() diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 6c243bdd1..fa651b810 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -100,6 +100,25 @@ def trace_error_handler(logger): logger.error(log_string) +def path_to_module(path): + """ + Convert a path to a module name (i.e openlp.core.common) + + :param pathlib.Path path: The path to convert to a module name. + :return: The module name. + :rtype: str + """ + module_path = path.with_suffix('') + return 'openlp.' + '.'.join(module_path.parts) + + +def import_openlp_module(module_name): + """ + Refactor module import out for testability. In Python 3.11, mock.patch and import_module do not play along nicely. + """ + importlib.import_module(module_name) + + def extension_loader(glob_pattern, excluded_files=None): """ A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and @@ -119,25 +138,13 @@ def extension_loader(glob_pattern, excluded_files=None): log.debug('Attempting to import %s', extension_path) module_name = path_to_module(extension_path) try: - importlib.import_module(module_name) + import_openlp_module(module_name) except (ImportError, OSError): # On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X) log.exception('Failed to import {module_name} on path {extension_path}' .format(module_name=module_name, extension_path=extension_path)) -def path_to_module(path): - """ - Convert a path to a module name (i.e openlp.core.common) - - :param pathlib.Path path: The path to convert to a module name. - :return: The module name. - :rtype: str - """ - module_path = path.with_suffix('') - return 'openlp.' + '.'.join(module_path.parts) - - def get_frozen_path(frozen_option, non_frozen_option): """ Return a path based on the system status. diff --git a/openlp/core/common/enum.py b/openlp/core/common/enum.py index a693925f7..2ce440802 100644 --- a/openlp/core/common/enum.py +++ b/openlp/core/common/enum.py @@ -112,6 +112,15 @@ class ServiceItemType(IntEnum): Image = 2 Command = 3 + @staticmethod + def parse(value): + if value in [1, '1', 'Text', 'ServiceItemType.Text']: + return ServiceItemType.Text + elif value in [2, '2', 'Image', 'ServiceItemType.Image']: + return ServiceItemType.Image + elif value in [3, '3', 'Command', 'ServiceItemType.Command']: + return ServiceItemType.Command + @unique class PluginStatus(IntEnum): diff --git a/openlp/core/common/i18n.py b/openlp/core/common/i18n.py index 43b37170e..733450d8c 100644 --- a/openlp/core/common/i18n.py +++ b/openlp/core/common/i18n.py @@ -22,6 +22,7 @@ The :mod:`languages` module provides a list of language names with utility functions. """ import itertools +import locale import logging import re from collections import namedtuple @@ -51,7 +52,8 @@ def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication. Language = namedtuple('Language', ['id', 'name', 'code']) -COLLATOR = None +ICU_COLLATOR = None +DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+') LANGUAGES = sorted([ Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'), Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'), @@ -503,19 +505,25 @@ def format_time(text, local_time): return re.sub(r'%[a-zA-Z]', match_formatting, text) -def get_locale_key(string, numeric=False): +def get_locale_key(string): """ Creates a key for case insensitive, locale aware string sorting. :param string: The corresponding string. """ string = string.lower() - global COLLATOR - if COLLATOR is None: - language = LanguageManager.get_language() - COLLATOR = QtCore.QCollator(QtCore.QLocale(language)) - COLLATOR.setNumericMode(numeric) - return COLLATOR.sortKey(string) + # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases. + global ICU_COLLATOR + try: + if ICU_COLLATOR is None: + import icu + language = LanguageManager.get_language() + icu_locale = icu.Locale(language) + ICU_COLLATOR = icu.Collator.createInstance(icu_locale) + return ICU_COLLATOR.getSortKey(string) + except Exception: + log.warning('ICU not found! Fallback to strxfrm') + return locale.strxfrm(string).encode() def get_natural_key(string): @@ -525,7 +533,13 @@ def get_natural_key(string): :param string: string to be sorted by Returns a list of string compare keys and integers. """ - return get_locale_key(string, True) + key = DIGITS_OR_NONDIGITS.findall(string) + key = [int(part) if part.isdigit() else get_locale_key(part) for part in key] + # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str + # and int. + if string and string[0].isdigit(): + return [b''] + key + return key def get_language(name): diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 9fd96c373..a407fc195 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -291,6 +291,8 @@ class Settings(QtCore.QSettings): 'media/vlc arguments': '', 'media/live volume': 50, 'media/preview volume': 0, + 'media/live loop': False, + 'media/preview loop': False, 'media/db type': 'sqlite', 'media/db username': '', 'media/db password': '', diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 81e706c05..15ce36312 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -463,7 +463,7 @@ class ServiceItem(RegistryProperties): header = service_item['serviceitem']['header'] self.title = header['title'] self.name = header['name'] - self.service_item_type = header['type'] + self.service_item_type = ServiceItemType.parse(header['type']) self.theme = header['theme'] self.add_icon() self.raw_footer = header['footer'] @@ -891,7 +891,7 @@ class ServiceItem(RegistryProperties): data_dict = { 'title': self.title, 'name': self.name, - 'type': str(self.service_item_type), + 'type': self.service_item_type, 'theme': self.theme, 'footer': self.raw_footer, 'audit': self.audit, diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index ee3ad71c5..a47dd3c20 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -23,6 +23,8 @@ The :mod:`~openlp.core.ui.media` module contains classes and objects for media p """ import logging +from openlp.core.common.registry import Registry + log = logging.getLogger(__name__ + '.__init__') # Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source @@ -83,9 +85,7 @@ class ItemMediaInfo(object): This class hold the media related info """ file_info = None - volume = 100 is_background = False - is_looping_playback = False length = 0 start_time = 0 end_time = 0 @@ -97,6 +97,57 @@ class ItemMediaInfo(object): media_type = MediaType() +def get_volume(controller) -> int: + """ + The volume needs to be retrieved + + :param controller: the controller in use + :return: Are we looping + """ + if controller.is_live: + return Registry().get('settings').value('media/live volume') + else: + return Registry().get('settings').value('media/preview volume') + + +def save_volume(controller, volume: int) -> None: + """ + The volume needs to be saved + + :param controller: the controller in use + :param volume: The volume to use and save + :return: Are we looping + """ + if controller.is_live: + return Registry().get('settings').setValue('media/live volume', volume) + else: + return Registry().get('settings').setValue('media/preview volume', volume) + + +def is_looping_playback(controller) -> bool: + """ + :param controller: the controller in use + :return: Are we looping + """ + if controller.is_live: + return Registry().get('settings').value('media/live loop') + else: + return Registry().get('settings').value('media/preview loop') + + +def toggle_looping_playback(controller) -> None: + """ + + :param controller: the controller in use + :return: None + """ + if controller.is_live: + Registry().get('settings').setValue('media/live loop', not Registry().get('settings').value('media/live loop')) + else: + Registry().get('settings').setValue('media/preview loop', + not Registry().get('settings').value('media/preview loop')) + + def parse_optical_path(input_string): """ Split the optical path info. diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 19422a7bd..23b10d9e2 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -42,7 +42,8 @@ from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.state import State from openlp.core.ui import DisplayControllerType, HideMode -from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path +from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, parse_stream_path, \ + get_volume, toggle_looping_playback, is_looping_playback, save_volume from openlp.core.ui.media.remote import register_views from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc @@ -236,10 +237,6 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): self.media_reset(controller) controller.media_info = ItemMediaInfo() controller.media_info.media_type = MediaType.Video - if controller.is_live: - controller.media_info.volume = self.settings.value('media/live volume') - else: - controller.media_info.volume = self.settings.value('media/preview volume') # background will always loop video. if service_item.is_capable(ItemCapabilities.HasBackgroundAudio): controller.media_info.file_info = [file_path for (file_path, file_hash) in service_item.background_audio] @@ -255,7 +252,6 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): elif service_item.is_capable(ItemCapabilities.HasBackgroundVideo): controller.media_info.file_info = [service_item.video_file_name] service_item.media_length = self.media_length(service_item.video_file_name) - controller.media_info.is_looping_playback = True controller.media_info.is_background = True else: controller.media_info.file_info = [service_item.get_frame_path()] @@ -457,13 +453,13 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): controller.seek_slider.blockSignals(False) controller.volume_slider.blockSignals(False) return False - self.media_volume(controller, controller.media_info.volume) + self.media_volume(controller, get_volume(controller)) if not start_hidden: self._media_set_visibility(controller, True) controller.mediabar.actions['playbackPlay'].setVisible(False) controller.mediabar.actions['playbackPause'].setVisible(True) controller.mediabar.actions['playbackStop'].setDisabled(False) - controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.is_looping_playback) + controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller)) controller.mediabar.actions['playbackStop'].setVisible(not controller.media_info.is_background or controller.media_info.media_type is MediaType.Audio) controller.mediabar.actions['playbackLoop'].setVisible((not controller.media_info.is_background and @@ -501,7 +497,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): if controller.media_info.is_playing and controller.media_info.length > 0: controller.media_info.timer += TICK_TIME if controller.media_info.timer >= controller.media_info.start_time + controller.media_info.length: - if controller.media_info.is_looping_playback: + if is_looping_playback(controller): start_again = True else: self.media_stop(controller) @@ -522,8 +518,11 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): end_seconds = controller.media_info.end_time // 1000 end_minutes = end_seconds // 60 end_seconds %= 60 - controller.position_label.setText(' %02d:%02d / %02d:%02d' % - (minutes, seconds, end_minutes, end_seconds)) + if end_minutes == 0 and end_seconds == 0: + controller.position_label.setText('') + else: + controller.position_label.setText(' %02d:%02d / %02d:%02d' % + (minutes, seconds, end_minutes, end_seconds)) def media_pause_msg(self, msg): """ @@ -573,8 +572,8 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): :param controller: The controller that needs to be stopped """ - controller.media_info.is_looping_playback = not controller.media_info.is_looping_playback - controller.mediabar.actions['playbackLoop'].setChecked(controller.media_info.is_looping_playback) + toggle_looping_playback(controller) + controller.mediabar.actions['playbackLoop'].setChecked(is_looping_playback(controller)) def media_stop_msg(self, msg): """ @@ -638,11 +637,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): :param volume: The volume to be set """ self.log_debug(f'media_volume {volume}') - if controller.is_live: - self.settings.setValue('media/live volume', volume) - else: - self.settings.setValue('media/preview volume', volume) - controller.media_info.volume = volume + save_volume(controller, volume) self.current_media_players[controller.controller_type].volume(controller, volume) controller.volume_slider.setValue(volume) diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 660d81829..b3c06ebd1 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -35,13 +35,11 @@ from openlp.core.common.i18n import translate from openlp.core.common.platform import is_linux, is_macosx, is_win from openlp.core.display.screens import ScreenList from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import MediaState, MediaType, VlCState +from openlp.core.ui.media import MediaState, MediaType, VlCState, get_volume from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) -# Audio and video extensions copied from 'include/vlc_interface.h' from vlc 2.2.0 source - STATE_WAIT_TIME = 60 @@ -229,7 +227,7 @@ class VlcPlayer(MediaPlayer): controller.vlc_media.parse() controller.seek_slider.setMinimum(controller.media_info.start_time) controller.seek_slider.setMaximum(controller.media_info.end_time) - self.volume(controller, controller.media_info.volume) + self.volume(controller, get_volume(controller)) return True def media_state_wait(self, controller, media_state): @@ -275,7 +273,7 @@ class VlcPlayer(MediaPlayer): threading.Thread(target=controller.vlc_media_player.play).start() if not self.media_state_wait(controller, VlCState.Playing): return False - self.volume(controller, controller.media_info.volume) + self.volume(controller, get_volume(controller)) self.set_state(MediaState.Playing, controller) return True diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 2d5162930..0b0b3784e 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -971,6 +971,8 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): Registry().execute( '{text}_start'.format(text=self.service_item.name.lower()), [self.service_item, self.is_live, self._current_hide_mode, slide_no]) + if self.service_item.is_capable(ItemCapabilities.ProvidesOwnTheme): + self._set_theme(self.service_item) else: self._set_theme(self.service_item) self.info_label.setText(self.service_item.title) diff --git a/openlp/plugins/alerts/alertsplugin.py b/openlp/plugins/alerts/alertsplugin.py index 8993a7655..90eca8b65 100644 --- a/openlp/plugins/alerts/alertsplugin.py +++ b/openlp/plugins/alerts/alertsplugin.py @@ -29,7 +29,6 @@ from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.lib.theme import VerticalType from openlp.core.lib.ui import create_action from openlp.core.ui.icons import UiIcons -from openlp.plugins.alerts.remote import register_views from openlp.plugins.alerts.forms.alertform import AlertForm from openlp.plugins.alerts.lib.alertsmanager import AlertsManager from openlp.plugins.alerts.lib.alertstab import AlertsTab @@ -139,7 +138,6 @@ class AlertsPlugin(Plugin): self.tools_alert_item.setVisible(True) action_list = ActionList.get_instance() action_list.add_action(self.tools_alert_item, UiStrings().Tools) - register_views() def finalise(self): """ diff --git a/openlp/plugins/alerts/remote.py b/openlp/plugins/alerts/remote.py deleted file mode 100644 index 6e6c6dea7..000000000 --- a/openlp/plugins/alerts/remote.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2023 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -from flask import Blueprint, request, abort - -from openlp.core.api import app -from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth -from openlp.core.common.registry import Registry -from openlp.core.lib.plugin import PluginStatus - - -v1_views = Blueprint('v1-alert-plugin', __name__) -v2_views = Blueprint('v2-alert-plugin', __name__) - - -@v2_views.route('', methods=['POST']) -@login_required -def alert(): - data = request.json - if not data: - abort(400) - alert = data.get('text', '') - if alert: - if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: - Registry().get('alerts_manager').alerts_text.emit([alert]) - return '', 204 - abort(400) - - -@v1_views.route('') -@old_auth -def old_alert(): - alert = extract_request(request.args.get('data', ''), 'text') - if alert: - if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: - Registry().get('alerts_manager').alerts_text.emit([alert]) - return old_success_response() - - -def register_views(): - app.register_blueprint(v2_views, url_prefix='/api/v2/plugins/alerts') - app.register_blueprint(v1_views, url_prefix='/api/alert') diff --git a/openlp/plugins/media/remote.py b/openlp/plugins/media/remote.py deleted file mode 100644 index 0f1974308..000000000 --- a/openlp/plugins/media/remote.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2023 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from flask import abort, request, Blueprint, jsonify - -from openlp.core.api import app -from openlp.core.api.lib import login_required, extract_request, old_auth -from openlp.core.lib.plugin import PluginStatus -from openlp.core.common.registry import Registry - -log = logging.getLogger(__name__) - - -v1_media = Blueprint('v1-media-plugin', __name__) -v2_media = Blueprint('v2-media-plugin', __name__) - - -def search(text): - plugin = Registry().get('plugin_manager').get_plugin_by_name('media') - if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: - results = plugin.media_item.search(text, False) - return results - return None - - -def live(id): - plugin = Registry().get('plugin_manager').get_plugin_by_name('media') - if plugin.status == PluginStatus.Active and plugin.media_item: - plugin.media_item.media_go_live.emit([id, True]) - - -def add(id): - plugin = Registry().get('plugin_manager').get_plugin_by_name('media') - if plugin.status == PluginStatus.Active and plugin.media_item: - item_id = plugin.media_item.create_item_from_id(id) - plugin.media_item.media_add_to_service.emit([item_id, True]) - - -@v2_media.route('/search') -@login_required -def search_view(): - text = request.args.get('text', '') - result = search(text) - return jsonify(result) - - -@v2_media.route('/add', methods=['POST']) -@login_required -def add_view(): - data = request.json - if not data: - abort(400) - id = data.get('id', -1) - add(id) - return '', 204 - - -@v2_media.route('/live', methods=['POST']) -@login_required -def live_view(): - data = request.json - if not data: - abort(400) - id = data.get('id', -1) - live(id) - return '', 204 - - -# ----------------- DEPRECATED -------------- -@v1_media.route('/search') -@old_auth -def old_search(): - text = extract_request(request.args.get('data', ''), 'text') - return jsonify({'results': {'items': search(text)}}) - - -@v1_media.route('/add') -@old_auth -def old_add(): - id = extract_request(request.args.get('data', ''), 'id') - add(id) - return '', 204 - - -@v1_media.route('/live') -@old_auth -def old_live(): - id = extract_request(request.args.get('data', ''), 'id') - live(id) - return '', 204 -# ---------------- END DEPRECATED ---------------- - - -def register_views(): - app.register_blueprint(v2_media, url_prefix='/api/v2/plugins/media/') - app.register_blueprint(v1_media, url_prefix='/api/media/') diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index f9bf18a0e..3a9ffdf2f 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -24,7 +24,7 @@ presentations from a variety of document formats. """ import logging import os - +from pathlib import Path from openlp.core.common import extension_loader, sha256_file_hash from openlp.core.common.i18n import translate @@ -84,7 +84,9 @@ class PresentationPlugin(Plugin): has_old_scheme = True # Migrate each file presentation_paths = self.settings.value('presentations/presentations files') or [] - for path in presentation_paths: + for presentation_path in presentation_paths: + # Typecast to Path object + path = Path(presentation_path) # check to see if the file exists before trying to process it. if not path.exists(): continue diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index e67b387bd..050dd9c28 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -52,7 +52,7 @@ def report_song_list(): report_file_path.with_suffix('.csv') Registry().get('application').set_busy_cursor() try: - with report_file_path.open('wt') as export_file: + with report_file_path.open('wt', encoding='utf8') as export_file: fieldnames = ('Title', 'Alternative Title', 'Copyright', 'Author(s)', 'Song Book', 'Topic') writer = csv.DictWriter(export_file, fieldnames=fieldnames, quoting=csv.QUOTE_ALL) headers = dict((n, n) for n in fieldnames) diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index c335d3d05..2c55662f4 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -52,6 +52,7 @@ WIN32_MODULES = [ 'win32com', 'win32ui', 'pywintypes', + 'icu', ] LINUX_MODULES = [ diff --git a/setup.py b/setup.py index b93741af1..098d93466 100644 --- a/setup.py +++ b/setup.py @@ -107,6 +107,7 @@ using a computer and a display/projector.""", 'lxml', 'Mako', "pillow", + 'PyICU', 'pymediainfo >= 2.2', 'pyobjc; platform_system=="Darwin"', 'pyobjc-framework-Cocoa; platform_system=="Darwin"', diff --git a/tests/openlp_core/api/test_tab.py b/tests/openlp_core/api/test_tab.py index aa2e35514..508885e93 100644 --- a/tests/openlp_core/api/test_tab.py +++ b/tests/openlp_core/api/test_tab.py @@ -35,15 +35,38 @@ from openlp.core.common.registry import Registry ZERO_URL = '0.0.0.0' +@pytest.fixture(scope="module") +def mocked_get_installed_version(): + setup_patcher = patch('openlp.core.api.tab.get_installed_version') + mocked_setup_patcher = setup_patcher.start() + mocked_setup_patcher.return_value = None + yield mocked_setup_patcher + setup_patcher.stop() + + @pytest.fixture -def api_tab(settings): - Registry().set_flag('website_version', '00-00-0000') - Registry().set_flag('no_web_server', False) +def api_tab_instantiate(mocked_get_installed_version, settings): + forms = [] parent = QtWidgets.QMainWindow() - form = ApiTab(parent) - yield form + + def _create_api_tab(): + nonlocal forms, parent + Registry().set_flag('website_version', '00-00-0000') + Registry().set_flag('no_web_server', False) + form = ApiTab(parent) + forms.append(form) + return form + + yield _create_api_tab del parent - del form + for form in forms: + del form + mocked_get_installed_version.return_value = None + + +@pytest.fixture +def api_tab(api_tab_instantiate): + yield api_tab_instantiate() def test_get_ip_address_default(api_tab): @@ -183,9 +206,12 @@ def test_available_version_property_set_none(api_tab): assert api_tab.available_version_value.text() == '(unknown)' -def test_installed_version_property_get_none(api_tab): +def test_installed_version_property_get_none(mocked_get_installed_version, api_tab_instantiate, settings): """Test that the installed version property is None on init""" # GIVEN: An uninitialised API tab + mocked_get_installed_version.return_value = None + settings.setValue('api/download_version', None) + api_tab = api_tab_instantiate() # WHEN: the installed version is GET'ed result = api_tab.installed_version diff --git a/tests/openlp_core/common/test_init.py b/tests/openlp_core/common/test_init.py index 750cd3d9c..fe5cc51f1 100644 --- a/tests/openlp_core/common/test_init.py +++ b/tests/openlp_core/common/test_init.py @@ -77,7 +77,7 @@ def test_extension_loader_files_found(): Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file2.py'), Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file3.py'), Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file4.py')]), \ - patch('openlp.core.common.importlib.import_module') as mocked_import_module: + patch('openlp.core.common.import_openlp_module') as mocked_import_module: # WHEN: Calling `extension_loader` with a list of files to exclude extension_loader('glob', ['file2.py', 'file3.py']) @@ -97,7 +97,7 @@ def test_extension_loader_import_error(): return_value=Path('/', 'app', 'dir', 'openlp')), \ patch.object(Path, 'glob', return_value=[ Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ - patch('openlp.core.common.importlib.import_module', side_effect=ImportError()), \ + patch('openlp.core.common.import_openlp_module', side_effect=ImportError()), \ patch('openlp.core.common.log') as mocked_logger: # WHEN: Calling `extension_loader` @@ -116,7 +116,7 @@ def test_extension_loader_os_error(): return_value=Path('/', 'app', 'dir', 'openlp')), \ patch.object(Path, 'glob', return_value=[ Path('/', 'app', 'dir', 'openlp', 'import_dir', 'file1.py')]), \ - patch('openlp.core.common.importlib.import_module', side_effect=OSError()), \ + patch('openlp.core.common.import_openlp_module', side_effect=OSError()), \ patch('openlp.core.common.log') as mocked_logger: # WHEN: Calling `extension_loader` diff --git a/tests/openlp_core/lib/test_lib.py b/tests/openlp_core/lib/test_lib.py index 4e12d7ea4..c46f5aa51 100644 --- a/tests/openlp_core/lib/test_lib.py +++ b/tests/openlp_core/lib/test_lib.py @@ -273,7 +273,7 @@ def test_image_to_byte_base_64(): assert 'byte_array base64ified' == result, 'The result should be the return value of the mocked base64 method' -def test_create_thumb_with_size(): +def test_create_thumb_with_size(registry): """ Test the create_thumb() function with a given size. """ @@ -308,7 +308,7 @@ def test_create_thumb_with_size(): pass -def test_create_thumb_no_size(): +def test_create_thumb_no_size(registry): """ Test the create_thumb() function with no size specified. """ @@ -343,7 +343,7 @@ def test_create_thumb_no_size(): pass -def test_create_thumb_invalid_size(): +def test_create_thumb_invalid_size(registry): """ Test the create_thumb() function with invalid size specified. """ @@ -379,7 +379,7 @@ def test_create_thumb_invalid_size(): pass -def test_create_thumb_width_only(): +def test_create_thumb_width_only(registry): """ Test the create_thumb() function with a size of only width specified. """ @@ -415,7 +415,7 @@ def test_create_thumb_width_only(): pass -def test_create_thumb_height_only(): +def test_create_thumb_height_only(registry): """ Test the create_thumb() function with a size of only height specified. """ @@ -451,7 +451,7 @@ def test_create_thumb_height_only(): pass -def test_create_thumb_empty_img(): +def test_create_thumb_empty_img(registry): """ Test the create_thumb() function with a size of only height specified. """ @@ -502,7 +502,7 @@ def test_create_thumb_empty_img(): @patch('openlp.core.lib.QtGui.QImageReader') @patch('openlp.core.lib.build_icon') -def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader): +def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader, registry): """ Test that build_icon() is run against the image_path when the thumbnail fails to be created """ @@ -537,7 +537,7 @@ def test_check_item_selected_true(): assert result is True, 'The result should be True' -def test_check_item_selected_false(): +def test_check_item_selected_false(registry): """ Test that the check_item_selected() function returns False when there are no selected indexes. """ @@ -608,7 +608,7 @@ def test_validate_thumb_file_exists_and_older(): assert result is False, 'The result should be False' -def test_resize_thumb(): +def test_resize_thumb(registry): """ Test the resize_thumb() function """ @@ -630,7 +630,7 @@ def test_resize_thumb(): assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.' -def test_resize_thumb_ignoring_aspect_ratio(): +def test_resize_thumb_ignoring_aspect_ratio(registry): """ Test the resize_thumb() function ignoring aspect ratio """ @@ -652,7 +652,7 @@ def test_resize_thumb_ignoring_aspect_ratio(): assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.' -def test_resize_thumb_width_aspect_ratio(): +def test_resize_thumb_width_aspect_ratio(registry): """ Test the resize_thumb() function using the image's width as the reference """ @@ -670,7 +670,7 @@ def test_resize_thumb_width_aspect_ratio(): assert wanted_width == result_size.width(), 'The image should have the requested width.' -def test_resize_thumb_same_aspect_ratio(): +def test_resize_thumb_same_aspect_ratio(registry): """ Test the resize_thumb() function when the image and the wanted aspect ratio are the same """ diff --git a/tests/openlp_core/lib/test_serviceitem.py b/tests/openlp_core/lib/test_serviceitem.py index 7c7c8c1de..630bfa551 100644 --- a/tests/openlp_core/lib/test_serviceitem.py +++ b/tests/openlp_core/lib/test_serviceitem.py @@ -862,7 +862,7 @@ def test_to_dict_text_item(mocked_sha256_file_hash, state_media, settings, servi ], 'theme': None, 'title': 'Amazing Grace', - 'type': 'ServiceItemType.Text', + 'type': ServiceItemType.Text, 'data': {'authors': 'John Newton', 'title': 'amazing grace@'} } assert result == expected_dict @@ -905,7 +905,7 @@ def test_to_dict_image_item(state_media, settings, service_item_env): ], 'theme': -1, 'title': 'Images', - 'type': 'ServiceItemType.Image', + 'type': ServiceItemType.Image, 'data': {} } assert result == expected_dict @@ -961,7 +961,7 @@ def test_to_dict_presentation_item(mocked_image_uri, mocked_get_data_path, state ], 'theme': None, 'title': '', - 'type': 'ServiceItemType.Command', + 'type': ServiceItemType.Command, 'data': {} } assert result == expected_dict diff --git a/tests/openlp_core/ui/media/test_mediacontroller.py b/tests/openlp_core/ui/media/test_mediacontroller.py index 81216d972..b5de7b257 100644 --- a/tests/openlp_core/ui/media/test_mediacontroller.py +++ b/tests/openlp_core/ui/media/test_mediacontroller.py @@ -22,14 +22,18 @@ Package to test the openlp.core.ui.media package. """ from pathlib import Path + +from unittest import skipUnless from unittest.mock import MagicMock, patch import pytest +from openlp.core.state import State +from openlp.core.common.platform import is_linux, is_macosx from openlp.core.common.registry import Registry from openlp.core.ui import DisplayControllerType, HideMode from openlp.core.ui.media.mediacontroller import MediaController -from openlp.core.ui.media import ItemMediaInfo, MediaState +from openlp.core.ui.media import ItemMediaInfo, MediaState, MediaType from tests.utils.constants import RESOURCE_PATH @@ -46,6 +50,216 @@ def media_env(registry): yield media_controller +@patch('openlp.core.ui.media.mediacontroller.register_views') +def test_setup(mocked_register_views, media_env): + """ + Test that the setup method is called correctly + """ + # GIVEN: A media controller, and function list + expected_functions_list = ['bootstrap_initialise', 'bootstrap_post_set_up', 'bootstrap_completion', + 'playbackPlay', 'playbackPause', 'playbackStop', 'playbackLoop', 'seek_slider', + 'volume_slider', 'media_hide', 'media_blank', 'media_unblank', 'songs_hide', + 'songs_blank', 'songs_unblank'] + # WHEN: Setup is called + media_env.setup() + # THEN: the functions should be defined along with the api blueprint + assert list(Registry().functions_list.keys()) == expected_functions_list, \ + f'The function list should have been {(Registry().functions_list.keys())}' + mocked_register_views.called_once(), 'The media blueprint has not been registered' + + +def test_initialise_good(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly + """ + # GIVEN: a mocked setup + with patch.object(media_env.media_controller, u'setup') as mocked_setup: + # THEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + + +def test_initialise_missing_vlc(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly with no VLC + """ + # GIVEN: a mocked setup and no VLC + with patch.object(media_env.media_controller, u'setup') as mocked_setup, \ + patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False): + # THEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + text = State().get_text() + if not is_macosx(): + assert text.find("python3-vlc") > 0, "VLC should not be missing" + + +@patch('openlp.core.ui.media.mediacontroller.pymediainfo_available', False) +def test_initialise_missing_pymedia(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly with no pymediainfo + """ + # GIVEN: a mocked setup and no pymedia + with patch.object(media_env.media_controller, u'setup') as mocked_setup: + # THEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + text = State().get_text() + if not is_macosx(): + assert text.find("python3-pymediainfo") > 0, "PyMedia should not be missing" + + +@skipUnless(is_linux(), "Linux only") +def test_initialise_missing_pymedia_fedora(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly with no VLC + """ + # GIVEN: a mocked setup and no VLC + with patch.object(media_env.media_controller, u'setup') as mocked_setup, \ + patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \ + patch('openlp.core.ui.media.mediacontroller.is_linux', return_value=True): + # WHEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + text = State().get_text() + assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing" + assert text.find("python3-vlc") > 0, "VLC should not be missing" + assert text.find("rpmfusion") > 0, "RPMFusion should provide the modules" + + +@skipUnless(is_linux(), "Linux only") +def test_initialise_missing_pymedia_not_fedora(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly with no VLC + """ + # GIVEN: a mocked setup and no VLC + with patch.object(media_env.media_controller, u'setup') as mocked_setup, \ + patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \ + patch('openlp.core.ui.media.mediacontroller.is_linux', return_value=False): + # WHEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + text = State().get_text() + assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing" + assert text.find("python3-vlc") > 0, "VLC should not be missing" + assert text.find("rpmfusion") == -1, "RPMFusion should not provide the modules" + + +def test_initialise_missing_pymedia_mac_os(media_env, state_media): + """ + Test that the bootstrap initialise method is called correctly with no VLC + """ + # GIVEN: a mocked setup and no VLC + with patch.object(media_env.media_controller, u'setup') as mocked_setup, \ + patch('openlp.core.ui.media.mediacontroller.get_vlc', return_value=False), \ + patch('openlp.core.ui.media.mediacontroller.is_macosx', return_value=True): + # WHEN: The underlying method is called + media_env.media_controller.bootstrap_initialise() + # THEN: The following should have happened + mocked_setup.called_once(), 'The setup function has been called' + text = State().get_text() + assert text.find("python3-pymediainfo") == -1, "PyMedia should be missing" + assert text.find("python3-vlc") == -1, "PyMedia should be missing" + assert text.find("videolan") > 0, "VideoLAN should provide the modules" + + +def test_post_set_up_good(media_env, state_media): + """ + Test the Bootstrap post set up assuming all functions are good + """ + # GIVEN: A working environment + media_env.vlc_live_media_stop = MagicMock() + media_env.vlc_preview_media_stop = MagicMock() + media_env.vlc_live_media_tick = MagicMock() + media_env.vlc_preview_media_tick = MagicMock() + State().add_service("mediacontroller", 0) + State().update_pre_conditions("mediacontroller", True) + # WHEN: I call the function + with patch.object(media_env.media_controller, u'setup_display') as mocked_display: + media_env.bootstrap_post_set_up() + # THEN: the environment is set up correctly + assert mocked_display.call_count == 2, "Should have been called twice" + text = State().get_text() + assert text.find("No Displays") == -1, "No Displays have been disable" + assert mocked_display.has_calls(None, False) # Live Controller + assert mocked_display.has_calls(None, True) # Preview Controller + + +def test_media_state_live(media_env, state_media): + """ + Test the Bootstrap post set up assuming all functions are good + """ + # GIVEN: A working environment + media_env.vlc_live_media_stop = MagicMock() + media_env.vlc_preview_media_stop = MagicMock() + media_env.vlc_preview_media_tick = MagicMock() + mocked_live_controller = MagicMock() + mocked_live_controller.is_live = True + mocked_live_controller.media_info.media_type = MediaType.Audio + Registry().register('live_controller', mocked_live_controller) + media_env.media_controller.vlc_player = MagicMock() + media_env.media_controller._display_controllers = MagicMock(return_value=mocked_live_controller) + # WHEN: I call the function + with patch.object(media_env.media_controller, u'setup_display') as mocked_display: + media_env.bootstrap_post_set_up() + # THEN: the environment is set up correctly + text = State().get_text() + assert text.find("No Displays") == -1, "No Displays have been disable" + assert mocked_display.has_calls(None, False) # Live Controller + assert mocked_display.has_calls(None, True) # Preview Controller + + +def test_post_set_up_no_controller(media_env, state_media): + """ + Test the Bootstrap post set up assuming all functions are good + """ + # GIVEN: A working environment + media_env.vlc_live_media_stop = MagicMock() + media_env.vlc_preview_media_stop = MagicMock() + media_env.vlc_live_media_tick = MagicMock() + media_env.vlc_preview_media_tick = MagicMock() + State().add_service("mediacontroller", 0) + State().update_pre_conditions("mediacontroller", False) + # WHEN: I call the function + with patch.object(media_env.media_controller, u'setup_display') as mocked_display: + media_env.bootstrap_post_set_up() + # THEN: the environment is set up correctly + assert mocked_display.call_count == 0, "Should have not have been called twice" + text = State().get_text() + assert text.find("No Displays") == -1, "No Displays have been disable" + + +def test_post_set_up_controller_exception(media_env, state_media): + """ + Test the Bootstrap post set up assuming all functions are good + """ + # GIVEN: A working environment + media_env.vlc_live_media_stop = MagicMock() + media_env.vlc_preview_media_stop = MagicMock() + media_env.vlc_live_media_tick = MagicMock() + media_env.vlc_preview_media_tick = MagicMock() + State().add_service("mediacontroller", 0) + State().update_pre_conditions("mediacontroller", True) + State().add_service("media_live", 0) + State().update_pre_conditions("media_live", True) + # WHEN: I call the function + with patch.object(media_env.media_controller, u'setup_display') as mocked_display: + mocked_display.side_effect = AttributeError() + try: + media_env.bootstrap_post_set_up() + except AttributeError: + pass + # THEN: the environment is set up correctly + text = State().get_text() + assert text.find("Displays") > 0, "Displays have been disable" + assert mocked_display.call_count == 2, "Should have been called twice" + + def test_resize(media_env): """ Test that the resize method is called correctly @@ -86,7 +300,6 @@ def test_load_video(media_env, settings): # The video should have autoplayed # The controls should have been made visible media_env.media_controller.media_reset.assert_called_once_with(mocked_slide_controller) - assert mocked_slide_controller.media_info.volume == 1 media_env.media_controller.media_play.assert_called_once_with(mocked_slide_controller, False) media_env.media_controller.set_controls_visible.assert_called_once_with(mocked_slide_controller, True) diff --git a/tests/openlp_core/ui/media/test_vlcplayer.py b/tests/openlp_core/ui/media/test_vlcplayer.py index bd3b4ef6d..57a64b7f1 100644 --- a/tests/openlp_core/ui/media/test_vlcplayer.py +++ b/tests/openlp_core/ui/media/test_vlcplayer.py @@ -283,7 +283,7 @@ def test_check_not_available(mocked_get_vlc): @patch('openlp.core.ui.media.vlcplayer.get_vlc') @patch('openlp.core.ui.media.vlcplayer.os.path.normcase') -def test_load(mocked_normcase, mocked_get_vlc): +def test_load(mocked_normcase, mocked_get_vlc, settings): """ Test loading a video into VLC """ @@ -294,7 +294,6 @@ def test_load(mocked_normcase, mocked_get_vlc): mocked_get_vlc.return_value = mocked_vlc mocked_display = MagicMock() mocked_controller = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.media_info.media_type = MediaType.Video mocked_controller.media_info.file_info.absoluteFilePath.return_value = media_path mocked_vlc_media = MagicMock() @@ -305,8 +304,7 @@ def test_load(mocked_normcase, mocked_get_vlc): vlc_player = VlcPlayer(None) # WHEN: A video is loaded into VLC - with patch.object(vlc_player, 'volume') as mocked_volume: - result = vlc_player.load(mocked_controller, mocked_display, media_path) + result = vlc_player.load(mocked_controller, mocked_display, media_path) # THEN: The video should be loaded mocked_normcase.assert_called_with(media_path) @@ -314,14 +312,13 @@ def test_load(mocked_normcase, mocked_get_vlc): assert mocked_vlc_media == mocked_controller.vlc_media mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() - mocked_volume.assert_called_with(mocked_controller, 100) assert result is True @patch('openlp.core.ui.media.vlcplayer.is_win') @patch('openlp.core.ui.media.vlcplayer.get_vlc') @patch('openlp.core.ui.media.vlcplayer.os.path.normcase') -def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win): +def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win, settings): """ Test loading an audio CD into VLC """ @@ -333,7 +330,6 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win): mocked_get_vlc.return_value = mocked_vlc mocked_display = MagicMock() mocked_controller = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.media_info.media_type = MediaType.CD mocked_controller.media_info.title_track = 1 mocked_vlc_media = MagicMock() @@ -351,8 +347,7 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win): vlc_player = VlcPlayer(None) # WHEN: An audio CD is loaded into VLC - with patch.object(vlc_player, 'volume') as mocked_volume, \ - patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: + with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: result = vlc_player.load(mocked_controller, mocked_display, media_path) # THEN: The video should be loaded @@ -361,7 +356,6 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win): assert mocked_vlc_media == mocked_controller.vlc_media mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() - mocked_volume.assert_called_with(mocked_controller, 100) mocked_media_state_wait.assert_called_with(mocked_controller, ANY) mocked_controller.seek_slider.setMinimum.assert_called_with(20000) mocked_controller.seek_slider.setMaximum.assert_called_with(30000) @@ -371,7 +365,7 @@ def test_load_audio_cd(mocked_normcase, mocked_get_vlc, mocked_is_win): @patch('openlp.core.ui.media.vlcplayer.is_win') @patch('openlp.core.ui.media.vlcplayer.get_vlc') @patch('openlp.core.ui.media.vlcplayer.os.path.normcase') -def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win): +def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win, settings): """ Test loading an audio CD into VLC on Windows """ @@ -383,7 +377,6 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win mocked_get_vlc.return_value = mocked_vlc mocked_display = MagicMock() mocked_controller = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.media_info.media_type = MediaType.CD mocked_controller.media_info.file_info.absoluteFilePath.return_value = media_path mocked_controller.media_info.title_track = 1 @@ -400,8 +393,7 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win vlc_player = VlcPlayer(None) # WHEN: An audio CD is loaded into VLC - with patch.object(vlc_player, 'volume') as mocked_volume, \ - patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: + with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: result = vlc_player.load(mocked_controller, mocked_display, media_path) # THEN: The video should be loaded @@ -410,7 +402,6 @@ def test_load_audio_cd_on_windows(mocked_normcase, mocked_get_vlc, mocked_is_win assert mocked_vlc_media == mocked_controller.vlc_media mocked_controller.vlc_media_player.set_media.assert_called_with(mocked_vlc_media) mocked_vlc_media.parse.assert_called_with() - mocked_volume.assert_called_with(mocked_controller, 100) mocked_media_state_wait.assert_called_with(mocked_controller, ANY) assert result is True @@ -449,7 +440,7 @@ def test_load_audio_cd_no_tracks(mocked_normcase, mocked_get_vlc, mocked_is_win) vlc_player = VlcPlayer(None) # WHEN: An audio CD is loaded into VLC - with patch.object(vlc_player, 'volume'), patch.object(vlc_player, 'media_state_wait'): + with patch.object(vlc_player, 'media_state_wait'): result = vlc_player.load(mocked_controller, mocked_display, media_path) # THEN: The video should be loaded @@ -465,7 +456,7 @@ def test_load_audio_cd_no_tracks(mocked_normcase, mocked_get_vlc, mocked_is_win) @patch('openlp.core.ui.media.vlcplayer.is_win') @patch('openlp.core.ui.media.vlcplayer.get_vlc') @patch('openlp.core.ui.media.vlcplayer.os.path.normcase') -def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win): +def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win, settings): """ Test loading a DVD into VLC """ @@ -477,14 +468,12 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win): mocked_get_vlc.return_value = mocked_vlc mocked_display = MagicMock() mocked_controller = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.media_info.media_type = MediaType.DVD mocked_controller.media_info.title_track = '2' mocked_controller.media_info.audio_track = 2 mocked_controller.media_info.subtitle_track = 4 mocked_vlc_media = MagicMock() mocked_media = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.media_info.start_time = 20000 mocked_controller.media_info.end_time = 30000 mocked_controller.media_info.length = 10000 @@ -493,8 +482,7 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win): vlc_player = VlcPlayer(None) # WHEN: A DVD clip is loaded into VLC - with patch.object(vlc_player, 'volume') as mocked_volume, \ - patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: + with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: result = vlc_player.load(mocked_controller, mocked_display, media_path) # THEN: The video should be loaded @@ -505,7 +493,6 @@ def test_load_dvd(mocked_normcase, mocked_get_vlc, mocked_is_win): mocked_controller.vlc_media_player.audio_set_track.assert_called_with(2) mocked_controller.vlc_media_player.video_set_spu.assert_called_with(4) mocked_vlc_media.parse.assert_called_with() - mocked_volume.assert_called_with(mocked_controller, 100) mocked_media_state_wait.assert_called_with(mocked_controller, ANY) mocked_controller.seek_slider.setMinimum.assert_called_with(20000) mocked_controller.seek_slider.setMaximum.assert_called_with(30000) @@ -606,7 +593,7 @@ def test_resize(): @patch('openlp.core.ui.media.vlcplayer.threading') @patch('openlp.core.ui.media.vlcplayer.get_vlc') -def test_play(mocked_get_vlc, mocked_threading): +def test_play(mocked_get_vlc, mocked_threading, settings): """ Test the play() method """ @@ -618,21 +605,17 @@ def test_play(mocked_get_vlc, mocked_threading): mocked_display = MagicMock() mocked_controller = MagicMock() mocked_media = MagicMock() - mocked_controller.media_info.volume = 100 mocked_controller.vlc_media_player.get_media.return_value = mocked_media vlc_player = VlcPlayer(None) vlc_player.set_state(MediaState.Paused, mocked_controller) # WHEN: play() is called - with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait, \ - patch.object(vlc_player, 'volume') as mocked_volume: + with patch.object(vlc_player, 'media_state_wait') as mocked_media_state_wait: mocked_media_state_wait.return_value = True result = vlc_player.play(mocked_controller, mocked_display) # THEN: A bunch of things should happen to play the media mocked_thread.start.assert_called_with() - mocked_volume.assert_called_with(mocked_controller, 100) - assert MediaState.Playing == vlc_player.get_live_state() assert result is True, 'The value returned from play() should be True' diff --git a/tests/openlp_core/ui/test_exceptionform.py b/tests/openlp_core/ui/test_exceptionform.py index fbdb7a2d5..689543be6 100644 --- a/tests/openlp_core/ui/test_exceptionform.py +++ b/tests/openlp_core/ui/test_exceptionform.py @@ -38,6 +38,7 @@ exceptionform.MIGRATE_VERSION = 'Migrate Test' exceptionform.CHARDET_VERSION = 'CHARDET Test' exceptionform.ENCHANT_VERSION = 'Enchant Test' exceptionform.MAKO_VERSION = 'Mako Test' +exceptionform.ICU_VERSION = 'ICU Test' exceptionform.VLC_VERSION = 'VLC Test' MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n' diff --git a/tests/openlp_core/ui/test_slidecontroller.py b/tests/openlp_core/ui/test_slidecontroller.py index 040087bcb..91689cfff 100644 --- a/tests/openlp_core/ui/test_slidecontroller.py +++ b/tests/openlp_core/ui/test_slidecontroller.py @@ -26,10 +26,11 @@ import datetime from unittest.mock import MagicMock, patch, sentinel from PyQt5 import QtCore, QtGui +from openlp.core.lib.serviceitem import ServiceItem from openlp.core.state import State from openlp.core.common.registry import Registry -from openlp.core.lib import ServiceItemAction +from openlp.core.lib import ItemCapabilities, ServiceItemAction from openlp.core.ui import HideMode from openlp.core.ui.slidecontroller import NON_TEXT_MENU, WIDE_MENU, NARROW_MENU, InfoLabel, LiveController, \ PreviewController, SlideController @@ -1141,6 +1142,54 @@ def test_process_item_is_reloading_wont_change_display_hide_mode(mocked_execute, slide_controller.set_hide_mode.assert_not_called() +@patch.object(Registry, 'execute') +def test_process_item_provides_own_theme(mocked_execute, registry, state_media): + """ + Test that media theme is set when media item is flagged with ProvidesOwnTheme + """ + # GIVEN: A mocked presentation service item that provides it's own theme, a mocked Registry.execute + # and a slide controller with many mocks. + mocked_pres_item = MagicMock() + mocked_pres_item.name = 'mocked_presentation_item' + mocked_pres_item.is_command.return_value = True + mocked_pres_item.is_media.return_value = False + mocked_pres_item.requires_media.return_value = False + mocked_pres_item.is_image.return_value = False + mocked_pres_item.is_text.return_value = False + # Needed to perform the capability checks + mocked_pres_item.is_capable = lambda param: ServiceItem.is_capable(mocked_pres_item, param) + mocked_pres_item.from_service = False + mocked_pres_item.capabilities = [ItemCapabilities.ProvidesOwnTheme] + mocked_pres_item.get_frames.return_value = [] + mocked_settings = MagicMock() + mocked_settings.value.return_value = True + mocked_main_window = MagicMock() + Registry().register('settings', mocked_settings) + Registry().register('main_window', mocked_main_window) + Registry().register('media_controller', MagicMock()) + Registry().register('application', MagicMock()) + slide_controller = SlideController(None) + slide_controller.service_item = mocked_pres_item + slide_controller.is_live = False + slide_controller.preview_widget = MagicMock() + slide_controller.preview_display = MagicMock() + slide_controller.enable_tool_bar = MagicMock() + slide_controller.slide_selected = MagicMock() + slide_controller.on_stop_loop = MagicMock() + slide_controller.info_label = MagicMock() + slide_controller._set_theme = MagicMock() + slide_controller.displays = [MagicMock()] + slide_controller.split = 0 + slide_controller.type_prefix = 'test' + slide_controller._current_hide_mode = None + + # WHEN: _process_item is called + slide_controller._process_item(mocked_pres_item, 0) + + # THEN: _set_theme should be called once + slide_controller._set_theme.assert_called_once() + + def test_live_stolen_focus_shortcuts(settings): """ Test that all the needed shortcuts are available in scenarios where Live has stolen focus. diff --git a/tests/openlp_plugins/alerts/test_plugin.py b/tests/openlp_plugins/alerts/test_plugin.py index ca248e775..dfc411857 100644 --- a/tests/openlp_plugins/alerts/test_plugin.py +++ b/tests/openlp_plugins/alerts/test_plugin.py @@ -32,8 +32,7 @@ from openlp.plugins.alerts.alertsplugin import AlertsPlugin def plugin_env(mocked_manager, settings, state, registry): """An instance of the AlertsPlugin""" mocked_manager.return_value = MagicMock() - with patch('openlp.plugins.alerts.alertsplugin.register_views'): - return AlertsPlugin(), settings + return AlertsPlugin(), settings def test_plugin_about(): @@ -77,12 +76,10 @@ def test_alerts_initialise(plugin_env): plugin = plugin_env[0] plugin.tools_alert_item = MagicMock() # WHEN: I request the form - with patch('openlp.core.common.actions.ActionList') as mocked_actionlist, \ - patch('openlp.plugins.alerts.alertsplugin.register_views') as mocked_register_views: + with patch('openlp.core.common.actions.ActionList') as mocked_actionlist: plugin.initialise() # THEN: the form is loaded mocked_actionlist.instance.add_action.assert_called_once() - mocked_register_views.assert_called_once_with() plugin.tools_alert_item.setVisible.assert_called_once_with(True) diff --git a/tests/openlp_plugins/presentations/test_plugin.py b/tests/openlp_plugins/presentations/test_plugin.py index 80a4af59e..16ede9359 100644 --- a/tests/openlp_plugins/presentations/test_plugin.py +++ b/tests/openlp_plugins/presentations/test_plugin.py @@ -21,7 +21,14 @@ """ This module contains tests for the plugin class Presentation plugin. """ +from pathlib import Path +from unittest.mock import MagicMock, patch + from openlp.plugins.presentations.presentationplugin import PresentationPlugin +from openlp.plugins.presentations.lib.presentationtab import PresentationTab + + +TEST_RESOURCES_PATH = Path(__file__) / '..' / '..' / '..' / 'resources' def test_plugin_about(): @@ -34,3 +41,33 @@ def test_plugin_about(): 'programs. The choice of available presentation programs is ' 'available to the user in a drop down box.' ) + + +def test_creaste_settings_tab(qapp, state, registry, settings): + """Test creating the settings tab""" + # GIVEN: A Presentations plugin + presentations_plugin = PresentationPlugin() + + # WHEN: create_settings_tab is run + presentations_plugin.create_settings_tab(None) + + # THEN: A settings tab should have been created + assert isinstance(presentations_plugin.settings_tab, PresentationTab) + + +@patch('openlp.plugins.presentations.presentationplugin.Manager') +def test_initialise(MockedManager, state, registry, mock_settings): + """Test that initialising the plugin works correctly""" + # GIVEN: Some initial values needed for intialisation and a presentations plugin + mock_settings.setValue.side_effect = [None, [str(TEST_RESOURCES_PATH / 'presentations' / 'test.ppt')]] + mocked_main_window = MagicMock() + registry.register('main_window', mocked_main_window) + presentations_plugin = PresentationPlugin() + presentations_plugin.media_item = MagicMock() + + # WHEN: initialise() is called + presentations_plugin.initialise() + + # THEN: Nothing should break, and everything should be called + mock_settings.setValue.assert_called_with('presentations/thumbnail_scheme', 'sha256file') + mock_settings.remove.assert_called_once_with('presentations/presentations files') diff --git a/tests/openlp_plugins/songs/test_reporting.py b/tests/openlp_plugins/songs/test_reporting.py index 5e18ab0ff..2456d3846 100644 --- a/tests/openlp_plugins/songs/test_reporting.py +++ b/tests/openlp_plugins/songs/test_reporting.py @@ -95,7 +95,9 @@ def test_report_song_list_error_reading(mock_file_dialog, mock_log, registry): Test that report song list sends an exception if the selected file location is not writable """ # GIVEN: A mocked file that returns a os error on open - def raise_os_error(x): + def raise_os_error(mode, encoding): + assert mode == 'wt' + assert encoding == 'utf8' raise OSError mock_file = MagicMock() mock_file.open.side_effect = raise_os_error