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