Merge branch 'add_footer_to_web_api' into 'master'

Add theme data and footer to web api

Closes #537

See merge request openlp/openlp!224
This commit is contained in:
Tomas Groth 2020-08-01 19:30:49 +00:00
commit c6c5f77b03
7 changed files with 268 additions and 25 deletions

View File

@ -24,7 +24,7 @@ from openlp.core.api.lib import login_required
from openlp.core.common import ThemeLevel
from openlp.core.common.registry import Registry
from flask import jsonify, request, abort, Blueprint
from flask import jsonify, request, abort, Blueprint, Response
controller_views = Blueprint('controller', __name__)
log = logging.getLogger(__name__)
@ -117,8 +117,10 @@ def set_theme_level():
@controller_views.route('/themes', methods=['GET'])
@login_required
def get_themes():
"""
Gets a list of all existing themes
"""
log.debug('controller-v2-themes-get')
theme_level = Registry().get('settings').value('themes/theme level')
theme_list = []
@ -144,11 +146,39 @@ def get_themes():
return jsonify(theme_list)
@controller_views.route('/themes/<theme_name>', methods=['GET'])
def get_theme_data(theme_name):
"""
Get a theme's data
"""
log.debug(f'controller-v2-theme-data-get {theme_name}')
themes = Registry().execute('get_theme_names')[0]
if theme_name not in themes:
log.error('Requested non-existent theme')
abort(404)
theme_data = Registry().get('theme_manager').get_theme_data(theme_name).export_theme_self_contained(True)
return Response(theme_data, mimetype='application/json')
@controller_views.route('/live-theme', methods=['GET'])
def get_live_theme_data():
"""
Get the live theme's data
"""
log.debug('controller-v2-live-theme-data-get')
live_service_item = Registry().get('live_controller').service_item
if live_service_item:
theme_data = live_service_item.get_theme_data()
else:
theme_data = Registry().get('theme_manager').get_theme_data(None)
self_contained_theme = theme_data.export_theme_self_contained(True)
return Response(self_contained_theme, mimetype='application/json')
@controller_views.route('/theme', methods=['GET'])
@login_required
def get_theme():
"""
Get the current theme
Get the current theme name
"""
log.debug('controller-v2-theme-get')
theme_level = Registry().get('settings').value('themes/theme level')

View File

@ -24,6 +24,7 @@ OpenLP work.
"""
import logging
import os
import base64
from enum import IntEnum
from pathlib import Path
@ -278,6 +279,17 @@ def image_to_byte(image, base_64=True):
return bytes(byte_array.toBase64()).decode('utf-8')
def image_to_data_uri(image_path):
"""
Converts a image into a base64 data uri
"""
extension = image_path.suffix.replace('.', '')
with open(image_path, 'rb') as image_file:
image_bytes = image_file.read()
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
return 'data:image/{extension};base64,{data}'.format(extension=extension, data=image_base64)
def create_thumb(image_path, thumb_path, return_icon=True, size=None):
"""
Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.

View File

@ -877,6 +877,7 @@ class ServiceItem(RegistryProperties):
item['text'] = frame['text']
item['html'] = self.rendered_slides[index]['text']
item['chords'] = self.rendered_slides[index]['chords']
item['footer'] = self.rendered_slides[index]['footer']
elif self.is_image() and not frame.get('image', '') and \
Registry().get('settings_thread').value('api/thumbnails'):
thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])

View File

@ -23,6 +23,7 @@ Provide the theme XML and handling functions for OpenLP v2 themes.
"""
import json
import logging
import copy
from lxml import etree, objectify
@ -30,7 +31,7 @@ from openlp.core.common import de_hump
from openlp.core.common.applocation import AppLocation
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
from openlp.core.display.screens import ScreenList
from openlp.core.lib import get_text_file_string, str_to_bool
from openlp.core.lib import get_text_file_string, str_to_bool, image_to_data_uri
log = logging.getLogger(__name__)
@ -390,6 +391,25 @@ class Theme(object):
theme_data["{attr}".format(attr=attr)] = value
return json.dumps(theme_data, cls=OpenLPJSONEncoder, base_path=theme_path, is_js=is_js)
def export_theme_self_contained(self, is_js=True):
"""
Get a self contained theme dictionary
Same as export theme, but images is turned into a data uri
:param is_js: For internal use, for example with the theme js code.
:return str: The json encoded theme object
"""
theme_copy = copy.deepcopy(self)
if self.background_type == 'image':
image = image_to_data_uri(self.background_filename)
theme_copy.background_filename = image
current_screen_geometry = ScreenList().current.display_geometry
theme_copy.display_size_width = current_screen_geometry.width()
theme_copy.display_size_height = current_screen_geometry.height()
theme_copy.background_source = ''
exported_theme = theme_copy.export_theme(is_js=is_js)
return exported_theme
def parse(self, xml):
"""
Read in an XML string and parse it.

View File

@ -18,16 +18,27 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import pytest
from unittest.mock import MagicMock
from openlp.core.common.registry import Registry
def test_retrieve_live_item(flask_client):
pytest.skip()
def test_retrieve_live_item(flask_client, settings):
"""
Test the live-item endpoint with a mocked service item
"""
# GIVEN: A mocked controller with a mocked service item
fake_live_controller = MagicMock()
fake_live_controller.service_item = MagicMock()
fake_live_controller.selected_row = 0
fake_live_controller.service_item.to_dict.return_value = {'slides': [{'selected': False}]}
Registry().register('live_controller', fake_live_controller)
# WHEN: The live-item endpoint is called
res = flask_client.get('/api/v2/controller/live-item').get_json()
assert len(res) == 0
# THEN: The correct item data should be returned
assert res == {'slides': [{'selected': True}]}
def test_controller_set_requires_login(settings, flask_client):
@ -42,6 +53,11 @@ def test_controller_set_does_not_accept_get(flask_client):
assert res.status_code == 405
def test_controller_set_aborts_on_unspecified_controller(flask_client, settings):
res = flask_client.post('/api/v2/controller/show')
assert res.status_code == 400
def test_controller_set_calls_live_controller(flask_client, settings):
fake_live_controller = MagicMock()
Registry().register('live_controller', fake_live_controller)
@ -67,6 +83,11 @@ def test_controller_direction_does_fails_on_wrong_data(flask_client, settings):
assert res.status_code == 400
def test_controller_direction_does_fails_on_missing_data(flask_client, settings):
res = flask_client.post('/api/v2/controller/progress')
assert res.status_code == 400
def test_controller_direction_calls_service_manager(flask_client, settings):
fake_live_controller = MagicMock()
Registry().register('live_controller', fake_live_controller)
@ -76,9 +97,22 @@ def test_controller_direction_calls_service_manager(flask_client, settings):
# Themes tests
def test_controller_get_theme_level_returns_valid_theme_level(flask_client, settings):
def test_controller_get_theme_level_returns_valid_theme_level_global(flask_client, settings):
settings.setValue('themes/theme level', 1)
res = flask_client.get('/api/v2/controller/theme-level').get_json()
assert res == ('global' or 'service' or 'song')
assert res == 'global'
def test_controller_get_theme_level_returns_valid_theme_level_service(flask_client, settings):
settings.setValue('themes/theme level', 2)
res = flask_client.get('/api/v2/controller/theme-level').get_json()
assert res == 'service'
def test_controller_get_theme_level_returns_valid_theme_level_song(flask_client, settings):
settings.setValue('themes/theme level', 3)
res = flask_client.get('/api/v2/controller/theme-level').get_json()
assert res == 'song'
def test_controller_set_theme_level_aborts_if_no_theme_level(flask_client, settings):
@ -91,12 +125,24 @@ def test_controller_set_theme_level_aborts_if_invalid_theme_level(flask_client,
assert res.status_code == 400
def test_controller_set_theme_level_sets_theme_level(flask_client, settings):
def test_controller_set_theme_level_sets_theme_level_global(flask_client, settings):
res = flask_client.post('/api/v2/controller/theme-level', json=dict(level='global'))
assert res.status_code == 204
assert Registry().get('settings').value('themes/theme level') == 1
def test_controller_set_theme_level_sets_theme_level_service(flask_client, settings):
res = flask_client.post('/api/v2/controller/theme-level', json=dict(level='service'))
assert res.status_code == 204
assert Registry().get('settings').value('themes/theme level') == 2
def test_controller_set_theme_level_sets_theme_level_song(flask_client, settings):
res = flask_client.post('/api/v2/controller/theme-level', json=dict(level='song'))
assert res.status_code == 204
assert Registry().get('settings').value('themes/theme level') == 3
def test_controller_get_themes_retrieves_themes_list(flask_client, settings):
Registry().register('theme_manager', MagicMock())
Registry().register('service_manager', MagicMock())
@ -104,25 +150,102 @@ def test_controller_get_themes_retrieves_themes_list(flask_client, settings):
assert type(res) is list
def test_controller_get_theme_returns_current_theme(flask_client, settings):
Registry().get('settings').setValue('themes/theme level', 1)
Registry().get('settings').setValue('themes/global theme', 'Default')
def test_controller_get_themes_retrieves_themes_list_service(flask_client, settings):
settings.setValue('themes/theme level', 2)
mocked_service_manager = MagicMock()
mocked_service_manager.service_theme = 'test_theme'
Registry().register('theme_manager', MagicMock())
Registry().register('service_manager', mocked_service_manager)
Registry().register_function('get_theme_names', MagicMock(side_effect=[['theme1', 'test_theme', 'theme2']]))
res = flask_client.get('api/v2/controller/themes').get_json()
assert res == [{'name': 'theme1', 'selected': False}, {'name': 'test_theme', 'selected': True},
{'name': 'theme2', 'selected': False}]
def test_controller_get_theme_data(flask_client, settings):
Registry().register_function('get_theme_names', MagicMock(side_effect=[['theme1', 'theme2']]))
Registry().register('theme_manager', MagicMock())
res = flask_client.get('api/v2/controller/themes/theme1')
assert res.status_code == 200
def test_controller_get_theme_data_invalid_theme(flask_client, settings):
Registry().register_function('get_theme_names', MagicMock(side_effect=[['theme1', 'theme2']]))
Registry().register('theme_manager', MagicMock())
res = flask_client.get('api/v2/controller/themes/imaginarytheme')
assert res.status_code == 404
def test_controller_get_live_theme_data(flask_client, settings):
fake_live_controller = MagicMock()
theme = MagicMock()
theme.export_theme_self_contained.return_value = '[[], []]'
fake_live_controller.service_item.get_theme_data.return_value = theme
Registry().register('live_controller', fake_live_controller)
res = flask_client.get('api/v2/controller/live-theme')
assert res.status_code == 200
assert res.get_json() == [[], []]
def test_controller_get_live_theme_data_no_service_item(flask_client, settings):
fake_theme_manager = MagicMock()
fake_live_controller = MagicMock()
theme = MagicMock()
theme.export_theme_self_contained.return_value = '[[], [], []]'
fake_theme_manager.get_theme_data.return_value = theme
fake_live_controller.service_item = None
Registry().register('theme_manager', fake_theme_manager)
Registry().register('live_controller', fake_live_controller)
res = flask_client.get('api/v2/controller/live-theme')
assert res.status_code == 200
assert res.get_json() == [[], [], []]
def test_controller_get_theme_returns_current_theme_global(flask_client, settings):
settings.setValue('themes/theme level', 1)
settings.setValue('themes/global theme', 'Default')
res = flask_client.get('/api/v2/controller/theme')
assert res.status_code == 200
assert res.get_json() == 'Default'
def test_controller_get_theme_returns_current_theme_service(flask_client, settings):
settings.setValue('themes/theme level', 2)
settings.setValue('servicemanager/service theme', 'Service')
res = flask_client.get('/api/v2/controller/theme')
assert res.status_code == 200
assert res.get_json() == 'Service'
def test_controller_set_theme_aborts_if_no_theme(flask_client, settings):
res = flask_client.post('/api/v2/controller/theme')
assert res.status_code == 400
def test_controller_set_theme_sets_theme(flask_client, settings):
def test_controller_set_theme_sets_global_theme(flask_client, settings):
settings.setValue('themes/theme level', 1)
res = flask_client.post('/api/v2/controller/theme', json=dict(theme='test'))
assert res.status_code == 204
def test_controller_set_theme_sets_service_theme(flask_client, settings):
settings.setValue('themes/theme level', 2)
res = flask_client.post('/api/v2/controller/theme', json=dict(theme='test'))
assert res.status_code == 204
def test_controller_set_theme_returns_song_exception(flask_client, settings):
Registry().get('settings').setValue('themes/theme level', 3)
settings.setValue('themes/theme level', 3)
res = flask_client.post('/api/v2/controller/theme', json=dict(theme='test'))
assert res.status_code == 501
def test_controller_clear_live(flask_client, settings):
Registry().register('live_controller', MagicMock())
res = flask_client.post('/api/v2/controller/clear/live')
assert res.status_code == 204
def test_controller_clear_invalid(flask_client, settings):
res = flask_client.post('/api/v2/controller/clear/my_screen')
assert res.status_code == 404

View File

@ -595,7 +595,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'That saved a wretch like me;\n'
'I once was lost, but now am found,\n'
'Was blind, but now I see.',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': 'Twas grace that taught my heart to fear,\n'
@ -612,7 +613,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
'The hour I first believed!',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': 'Through many dangers, toils and snares\n'
@ -629,7 +631,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': 'The Lord has promised good to me,\n'
@ -646,7 +649,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'His word my hope secures;\n'
'He will my shield and portion be\n'
'As long as life endures.',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': 'Yes, when this heart and flesh shall fail,\n'
@ -663,7 +667,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
'A life of joy and peace.',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
},
{
'chords': 'When weve been there a thousand years,\n'
@ -680,7 +685,8 @@ def test_to_dict_text_item(state_media, settings, service_item_env):
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'
'Than when we first begun.',
'title': 'Amazing Grace'
'title': 'Amazing Grace',
'footer': 'Amazing Grace<br>Written by: John Newton'
}
],
'theme': None,

View File

@ -25,7 +25,7 @@ import os
import shutil
from pathlib import Path
from tempfile import mkdtemp
from unittest.mock import ANY, Mock, MagicMock, patch, call
from unittest.mock import ANY, Mock, MagicMock, patch, call, sentinel
from PyQt5 import QtWidgets
@ -66,6 +66,56 @@ def test_initial_theme_manager(registry):
assert Registry().get('theme_manager') is not None, 'The base theme manager should be registered'
@patch('openlp.core.ui.thememanager.Theme')
def test_get_global_theme(mocked_theme, registry):
"""
Test the global theme method returns the theme data for the global theme
"""
# GIVEN: A service manager instance and the global theme
theme_manager = ThemeManager(None)
theme_manager.global_theme = 'global theme name'
theme_manager._theme_list = {'global theme name': sentinel.global_theme}
# WHEN: Calling get_global_theme
result = theme_manager.get_global_theme()
# THEN: Returned global theme
assert result == sentinel.global_theme
@patch('openlp.core.ui.thememanager.Theme')
def test_get_theme_data(mocked_theme, registry):
"""
Test that the get theme data method returns the requested theme data
"""
# GIVEN: A service manager instance and themes
theme_manager = ThemeManager(None)
theme_manager._theme_list = {'theme1': sentinel.theme1, 'theme2': sentinel.theme2}
# WHEN: Get theme data is called with 'theme2'
result = theme_manager.get_theme_data('theme2')
# THEN: Should return theme2's data
assert result == sentinel.theme2
@patch('openlp.core.ui.thememanager.Theme')
def test_get_theme_data_missing(mocked_theme, registry):
"""
Test that the get theme data method returns the default theme when theme name not found
"""
# GIVEN: A service manager instance and themes
theme_manager = ThemeManager(None)
theme_manager._theme_list = {'theme1': sentinel.theme1, 'theme2': sentinel.theme2}
mocked_theme.return_value = sentinel.default_theme
# WHEN: Get theme data is called with None
result = theme_manager.get_theme_data(None)
# THEN: Should return default theme's data
assert result == sentinel.default_theme
@patch('openlp.core.ui.thememanager.shutil')
@patch('openlp.core.ui.thememanager.create_paths')
def test_save_theme_same_image(mocked_create_paths, mocked_shutil, registry):
@ -301,7 +351,8 @@ def test_over_write_message_box_no(mocked_translate, mocked_qmessagebox_question
defaultButton=ANY)
def test_unzip_theme(registry):
@patch('openlp.core.lib.theme.Theme.set_default_header_footer')
def test_unzip_theme(mocked_theme_set_defaults, registry):
"""
Test that unzipping of themes works
"""