Expose the entire ServiceItem in the API

- Move JSON rendering into ServiceItem object
- Provide entire service item object through API
- Fix some potential bugs and write tests
This commit is contained in:
Raoul Snyman 2020-07-21 23:42:40 -07:00
parent 5a4a4e703f
commit 5620a6f57f
Signed by: raoul
GPG Key ID: F55BCED79626AE9C
6 changed files with 533 additions and 51 deletions

View File

@ -19,16 +19,10 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
import logging
import os
import urllib.request
from pathlib import Path
from openlp.core.api.lib import login_required
from openlp.core.common import ThemeLevel
from openlp.core.common.registry import Registry
from openlp.core.common.applocation import AppLocation
from openlp.core.lib import create_thumb
from openlp.core.lib.serviceitem import ItemCapabilities
from flask import jsonify, request, abort, Blueprint
@ -41,44 +35,11 @@ def controller_text_api():
log.debug('controller-v2-live-item')
live_controller = Registry().get('live_controller')
current_item = live_controller.service_item
data = []
live_item = {}
if current_item:
for index, frame in enumerate(current_item.get_frames()):
item = {}
item['tag'] = index + 1
item['selected'] = live_controller.selected_row == index
item['title'] = current_item.title
if current_item.is_text():
if frame['verse']:
item['tag'] = str(frame['verse'])
item['text'] = frame['text']
item['html'] = current_item.rendered_slides[index]['text']
item['chords'] = current_item.rendered_slides[index]['chords']
elif current_item.is_image() and not frame.get('image', '') and \
Registry().get('settings_thread').value('api/thumbnails'):
thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
if not full_thumbnail_path.exists():
create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False)
item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
else:
# presentations and other things
if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
item['title'] = str(frame['display_title'])
if current_item.is_capable(ItemCapabilities.HasNotes):
item['slide_notes'] = str(frame['notes'])
if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
Registry().get('settings_thread').value('api/thumbnails'):
# If the file is under our app directory tree send the portion after the match
data_path = str(AppLocation.get_data_path())
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
data.append(item)
return jsonify(data)
live_item = current_item.to_dict()
live_item['slides'][live_controller.selected_row]['selected'] = True
return jsonify(live_item)
@controller_views.route('/show', methods=['POST'])

View File

@ -25,6 +25,7 @@ import datetime
import logging
import ntpath
import os
import urllib.request
import uuid
from copy import deepcopy
from pathlib import Path
@ -32,7 +33,6 @@ from shutil import copytree, copy, move
from PyQt5 import QtGui
from openlp.core.state import State
from openlp.core.common import ThemeLevel, sha256_file_hash
from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import ServiceItemType
@ -41,7 +41,9 @@ from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
from openlp.core.lib import ItemCapabilities
from openlp.core.lib import create_thumb
from openlp.core.lib.theme import BackgroundType
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.media import parse_stream_path
@ -844,3 +846,59 @@ class ServiceItem(RegistryProperties):
elif self.is_image() and self.slides and 'thumbnail' in self.slides[0]:
return os.path.dirname(self.slides[0]['thumbnail'])
return None
def to_dict(self):
"""
Convert the service item into a dictionary
"""
data_dict = {
'title': self.title,
'name': self.name,
'type': str(self.service_item_type),
'theme': self.theme,
'footer': self.raw_footer,
'audit': self.audit,
'notes': self.notes,
'fromPlugin': self.from_plugin,
'capabilities': self.capabilities,
'backgroundAudio': [str(file_path) for file_path in self.background_audio],
'isThemeOverwritten': self.theme_overwritten,
'slides': []
}
for index, frame in enumerate(self.get_frames()):
item = {
'tag': index + 1,
'title': self.title,
'selected': False
}
if self.is_text():
if frame['verse']:
item['tag'] = str(frame['verse'])
item['text'] = frame['text']
item['html'] = self.rendered_slides[index]['text']
item['chords'] = self.rendered_slides[index]['chords']
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'])
full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path
if not full_thumbnail_path.exists():
create_thumb(Path(self.get_frame_path(index)), full_thumbnail_path, False)
item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path))
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
else:
# presentations and other things
if self.is_capable(ItemCapabilities.HasDisplayTitle):
item['title'] = str(frame['display_title'])
if self.is_capable(ItemCapabilities.HasNotes):
item['slide_notes'] = str(frame['notes'])
if self.is_capable(ItemCapabilities.HasThumbnails) and \
Registry().get('settings_thread').value('api/thumbnails'):
# If the file is under our app directory tree send the portion after the match
data_path = str(AppLocation.get_data_path())
if frame['image'][0:len(data_path)] == data_path:
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
item['text'] = str(frame['title'])
item['html'] = str(frame['title'])
data_dict['slides'].append(item)
return data_dict

View File

@ -417,10 +417,10 @@ class ProjectorDB(Manager):
"""
source_dict = {}
# Apparently, there was a change to the projector object. Test for which object has db id
if hasattr(projector, "id"):
chk = projector.id
elif hasattr(projector.entry, "id"):
if hasattr(projector, 'entry') and hasattr(projector.entry, 'id'):
chk = projector.entry.id
else:
chk = projector.id
# Get default list first
for key in projector.source_available:

View File

@ -76,10 +76,9 @@ def service_item_env(state):
def test_service_item_basic(settings):
"""
Test the Service Item - basic test
Test creating a new Service Item without a plugin
"""
# GIVEN: A new service item
# WHEN: A service item is created (without a plugin)
service_item = ServiceItem(None)
@ -88,6 +87,22 @@ def test_service_item_basic(settings):
assert service_item.missing_frames() is True, 'There should not be any frames in the service item'
def test_service_item_with_plugin(settings):
"""
Test creating a new Service Item with a plugin
"""
# GIVEN: A new service item
mocked_plugin = MagicMock()
mocked_plugin.name = 'songs'
# WHEN: A service item is created (with a plugin)
service_item = ServiceItem(mocked_plugin)
# THEN: We should get back a valid service item
assert service_item.name == 'songs', 'The service item name should be the same as the plugin name'
assert service_item.is_valid is True, 'The new service item should be valid'
assert service_item.missing_frames() is True, 'There should not be any frames in the service item'
def test_service_item_load_custom_from_service(state_media, settings, service_item_env):
"""
Test the Service Item - adding a custom slide from a saved service
@ -522,3 +537,249 @@ def test_service_item_get_theme_data_song_level_global_fallback(settings):
# THEN: theme should be the global theme
assert theme == mocked_theme_manager.global_theme
def test_remove_capability(settings):
# GIVEN: A service item with a capability
service_item = ServiceItem(None)
service_item.add_capability(ItemCapabilities.CanEdit)
assert ItemCapabilities.CanEdit in service_item.capabilities
# WHEN: A capability is removed
service_item.remove_capability(ItemCapabilities.CanEdit)
# THEN: The capability should no longer be there
assert ItemCapabilities.CanEdit not in service_item.capabilities, 'The capability should not be in the list'
def test_to_dict_text_item(state_media, settings, service_item_env):
"""
Test that the to_dict() method returns the correct data for the service item
"""
# GIVEN: A ServiceItem with a service loaded from file
mocked_plugin = MagicMock()
mocked_plugin.name = 'songs'
service_item = ServiceItem(mocked_plugin)
service_item.add_icon = MagicMock()
FormattingTags.load_tags()
line = convert_file_service_item(TEST_PATH, 'serviceitem-song-linked-audio.osj')
service_item.set_from_service(line, Path('/test/'))
# WHEN: to_dict() is called
result = service_item.to_dict()
# THEN: The correct dictionary should be returned
expected_dict = {
'audit': ['Amazing Grace', ['John Newton'], '', ''],
'backgroundAudio': ['/test/amazing_grace.mp3'],
'capabilities': [2, 1, 5, 8, 9, 13, 15],
'footer': ['Amazing Grace', 'Written by: John Newton'],
'fromPlugin': False,
'isThemeOverwritten': False,
'name': 'songs',
'notes': '',
'slides': [
{
'chords': 'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n'
'I once was lost, but now am found,\n'
'Was blind, but now I see.',
'html': 'Amazing Grace! how sweet the sound\n'
'That saved a wretch like me;\n'
'I once was lost, but now am found,\n'
'Was blind, but now I see.',
'selected': False,
'tag': 'V1',
'text': 'Amazing Grace! how sweet the sound\n'
'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'
},
{
'chords': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
'The hour I first believed!',
'html': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
'The hour I first believed!',
'selected': False,
'tag': 'V2',
'text': 'Twas grace that taught my heart to fear,\n'
'And grace my fears relieved;\n'
'How precious did that grace appear,\n'
'The hour I first believed!',
'title': 'Amazing Grace'
},
{
'chords': 'Through many dangers, toils and snares\n'
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.',
'html': 'Through many dangers, toils and snares\n'
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.',
'selected': False,
'tag': 'V3',
'text': 'Through many dangers, toils and snares\n'
'I have already come;\n'
'Tis grace that brought me safe thus far,\n'
'And grace will lead me home.',
'title': 'Amazing Grace'
},
{
'chords': 'The Lord has promised good to me,\n'
'His word my hope secures;\n'
'He will my shield and portion be\n'
'As long as life endures.',
'html': 'The Lord has promised good to me,\n'
'His word my hope secures;\n'
'He will my shield and portion be\n'
'As long as life endures.',
'selected': False,
'tag': 'V4',
'text': 'The Lord has promised good to me,\n'
'His word my hope secures;\n'
'He will my shield and portion be\n'
'As long as life endures.',
'title': 'Amazing Grace'
},
{
'chords': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
'A life of joy and peace.',
'html': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
'A life of joy and peace.',
'selected': False,
'tag': 'V5',
'text': 'Yes, when this heart and flesh shall fail,\n'
'And mortal life shall cease,\n'
'I shall possess within the veil\n'
'A life of joy and peace.',
'title': 'Amazing Grace'
},
{
'chords': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'
'Than when we first begun.',
'html': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'
'Than when we first begun.',
'selected': False,
'tag': 'V6',
'text': 'When weve been there a thousand years,\n'
'Bright shining as the sun,\n'
'Weve no less days to sing Gods praise\n'
'Than when we first begun.',
'title': 'Amazing Grace'
}
],
'theme': None,
'title': 'Amazing Grace',
'type': 'ServiceItemType.Text'
}
assert result == expected_dict
def test_to_dict_image_item(state_media, settings, service_item_env):
"""
Test that the to_dict() method returns the correct data for the service item
"""
# GIVEN: A ServiceItem with a service loaded from file
mocked_plugin = MagicMock()
mocked_plugin.name = 'image'
service_item = ServiceItem(mocked_plugin)
service_item.add_icon = MagicMock()
FormattingTags.load_tags()
line = convert_file_service_item(TEST_PATH, 'serviceitem_image_2.osj')
with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash:
mocked_sha256_file_hash.return_value = '3a7ccbdb0b5a3db169c4692d7aad0ec8'
service_item.set_from_service(line)
# WHEN: to_dict() is called
result = service_item.to_dict()
# THEN: The correct dictionary should be returned
expected_dict = {
'audit': '',
'backgroundAudio': [],
'capabilities': [3, 1, 5, 6],
'footer': [],
'fromPlugin': False,
'isThemeOverwritten': False,
'name': 'images',
'notes': '',
'slides': [
{
'html': 'image_1.jpg',
'img': '/images/thumbnails/image_1.jpg',
'selected': False,
'tag': 1,
'text': 'image_1.jpg',
'title': 'Images'
}
],
'theme': -1,
'title': 'Images',
'type': 'ServiceItemType.Image'
}
assert result == expected_dict
def test_to_dict_presentation_item(state_media, settings, service_item_env):
"""
Test that the to_dict() method returns the correct data for the service item
"""
# GIVEN: A ServiceItem with a service loaded from file
mocked_plugin = MagicMock()
mocked_plugin.name = 'presentations'
service_item = ServiceItem(mocked_plugin)
presentation_name = 'test.pptx'
image = Path('thumbnails/abcd/slide1.png')
display_title = 'DisplayTitle'
notes = 'Note1\nNote2\n'
# WHEN: adding presentation to service_item
with patch('openlp.core.lib.serviceitem.sha256_file_hash') as mocked_sha256_file_hash,\
patch('openlp.core.lib.serviceitem.AppLocation.get_section_data_path') as mocked_get_section_data_path:
mocked_sha256_file_hash.return_value = '4a067fed6834ea2bc4b8819f11636365'
mocked_get_section_data_path.return_value = Path('.')
service_item.add_from_command(TEST_PATH, presentation_name, image, display_title, notes)
# WHEN: to_dict() is called
result = service_item.to_dict()
# THEN: The correct dictionary should be returned
expected_dict = {
'audit': '',
'backgroundAudio': [],
'capabilities': [],
'footer': [],
'fromPlugin': False,
'isThemeOverwritten': False,
'name': 'presentations',
'notes': '',
'slides': [
{
'html': 'test.pptx',
'selected': False,
'tag': 1,
'text': 'test.pptx',
'title': ''
}
],
'theme': None,
'title': '',
'type': 'ServiceItemType.Command'
}
assert result == expected_dict

View File

@ -29,6 +29,26 @@ Test the States class.
"""
def test_load_settings(state):
# GIVEN: Some modules
State().modules = {'mock': MagicMock}
# WHEN: load_settings() is run
State().load_settings()
# THEN: the modules should be empty
assert State().modules == {}, 'There should be no modules in the State object'
def test_save_settings(state):
# GIVEN: Niks
# WHEN: save_settings() is run
State().save_settings()
# THEN: Nothing should happen
assert True, 'There should be no exceptions'
def test_add_service(state):
# GIVEN a new state
# WHEN I add a new service
@ -128,3 +148,62 @@ def test_basic_preconditions_pass(state, registry):
assert State().modules['test'].pass_preconditions is True
assert State().modules['test2'].pass_preconditions is False
assert State().modules['test1'].pass_preconditions is True
def test_missing_text(state):
"""
Test that settings the missing text in a module works
"""
# GIVEN: A state with a module
State().modules['test'] = MagicMock()
# WHEN: missing_text() is called
State().missing_text('test', 'Test test')
# THEN: The text is set
assert State().modules['test'].text == 'Test test', 'The text on the module should have been set'
def test_get_text(state):
"""
Test that the get_text() method returns the text of all the states
"""
# GIVEN: Some states with text
State().modules.update({'test1': MagicMock(text='Test 1'), 'test2': MagicMock(text='Test 2')})
# WHEN: get_text() is called
result = State().get_text()
# THEN: The correct text is returned
assert result == 'Test 1\nTest 2\n', 'The full text is returned'
def test_check_preconditions_no_required(state):
"""
Test that the check_preconditions() method returns the correct attribute when there are no requirements
"""
# GIVEN: A State with no requires
State().modules.update({'test_pre1': MagicMock(requires=None, pass_preconditions=True)})
# WHEN: check_preconditions() is called
result = State().check_preconditions('test_pre1')
# THEN: The correct result should be returned
assert result is True
def test_check_preconditions_required_module(state):
"""
Test that the check_preconditions() method returns the correct attribute when there is another required module
"""
# GIVEN: A State with two modules
State().modules.update({
'test_pre2': MagicMock(requires='test_pre3', pass_preconditions=True),
'test_pre3': MagicMock(requires=None, pass_preconditions=False)
})
# WHEN: check_preconditions() is called
result = State().check_preconditions('test_pre2')
# THEN: The correct result should be returned
assert result is False

View File

@ -27,7 +27,7 @@ PREREQUISITE: add_record() and get_all() functions validated.
import pytest
import os
import shutil
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from openlp.core.lib.db import upgrade_db
from openlp.core.projectors import upgrade
@ -321,6 +321,21 @@ def test_get_projector_by_id_none(projector):
def test_get_projector_all_none(projector):
"""
Test get_projector_all() when self.get_all_objects() returns None
"""
# GIVEN: Mocked out get_all_objects
with patch.object(projector, 'get_all_objects') as mocked_get_all_objects:
mocked_get_all_objects.return_value = None
# WHEN: We retrieve the database entries
results = projector.get_projector_all()
# THEN: Verify results is empty
assert [] == results, 'Returned results should have returned an empty list'
def test_get_projector_all_empty(projector):
"""
Test get_projector_all() with no projectors in db
"""
@ -329,7 +344,7 @@ def test_get_projector_all_none(projector):
# WHEN: We retrieve the database entries
results = projector.get_projector_all()
# THEN: Verify results is None
# THEN: Verify results is empty
assert [] == results, 'Returned results should have returned an empty list'
@ -436,3 +451,111 @@ def test_delete_projector_fail(projector):
# THEN: Results should be False
assert results is False, 'delete_projector() should have returned False'
def test_get_source_list_no_sources(projector):
"""
Test that an empty source list is returned
"""
# GIVEN: A mocked projector
mocked_projector = MagicMock(id='1', source_available=[])
# WHEN: get_source_list is run
results = projector.get_source_list(mocked_projector)
# THEN: The list should be empty
assert results == {}, 'The list of sources returned should be empty'
def test_get_source_list_source_is_none(projector):
"""
Test that a default code is returned when a source is not in the database
"""
# GIVEN: A mocked projector
mocked_projector = MagicMock(id='1', source_available=['11'])
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = None
# WHEN: get_source_list is run
results = projector.get_source_list(mocked_projector)
# THEN: The list should contain the one default item
assert results == {'11': 'RGB 1'}, 'The list of sources returned should contain "RGB1"'
def test_get_source_list_source_has_item(projector):
"""
Test that a default code is returned when a source is in the database
"""
# GIVEN: A mocked projector
mocked_projector = MagicMock(entry=MagicMock(id='1'), source_available=['5Y'])
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = MagicMock(text='VGA 1')
# WHEN: get_source_list is run
results = projector.get_source_list(mocked_projector)
# THEN: The list should contain the one default item
assert results == {'5Y': 'VGA 1'}, 'The list of sources returned should contain "RGB1"'
def test_get_source_by_id_none(projector):
"""
Test that no source in the db returns None
"""
# GIVEN: A Mocked get_object_filtered method
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = None
# WHEN: Get the source by ID
result = projector.get_source_by_id('source')
# THEN: The result should be None
assert result is None, 'None should be returned by get_source_by_id'
def test_get_source_by_id(projector):
"""
Test that a source in the db returns that source
"""
# GIVEN: A Mocked get_object_filtered method
mocked_entry = MagicMock()
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = mocked_entry
# WHEN: Get the source by ID
result = projector.get_source_by_id('source')
# THEN: The result should be the mocked entry
assert result is mocked_entry, 'The mocked entry should be returned by get_source_by_id'
def test_get_source_by_code_none(projector):
"""
Test that no source in the db returns None
"""
# GIVEN: A Mocked get_object_filtered method
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = None
# WHEN: Get the source by code
result = projector.get_source_by_code('11', 52)
# THEN: The result should be None
assert result is None, 'None should be returned by get_source_by_code'
def test_get_source_by_code(projector):
"""
Test that a source in the db returns that source
"""
# GIVEN: A Mocked get_object_filtered method
mocked_entry = MagicMock()
with patch.object(projector, 'get_object_filtered') as mocked_get_object_filtered:
mocked_get_object_filtered.return_value = mocked_entry
# WHEN: Get the source by code
result = projector.get_source_by_code('5Y', 12)
# THEN: The result should be the mocked entry
assert result is mocked_entry, 'The mocked entry should be returned by get_source_by_code'