Adding Footer Content as Extra First Slide

This commit is contained in:
Mateus Meyer Jiacomelli 2023-05-20 14:31:59 +00:00 committed by Raoul Snyman
parent 9e34f9eca1
commit d9911790d4
12 changed files with 369 additions and 156 deletions

View File

@ -146,3 +146,13 @@ class SongSearch(IntEnum):
Themes = 7
Copyright = 8
CCLInumber = 9
@unique
class SongFirstSlideMode(IntEnum):
"""
An enumeration for song first slide types.
"""
Default = 0 # No cover
Songbook = 1
Footer = 2

View File

@ -33,7 +33,7 @@ from PyQt5 import QtCore, QtGui
from openlp.core.common import SlideLimits, ThemeLevel
from openlp.core.common.enum import AlertLocation, BibleSearch, CustomSearch, ImageThemeMode, LayoutStyle, \
DisplayStyle, LanguageSelection, SongSearch, PluginStatus
DisplayStyle, LanguageSelection, SongFirstSlideMode, SongSearch, PluginStatus
from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder, is_serializable
from openlp.core.common.path import files_to_paths, str_to_path
from openlp.core.common.platform import is_linux, is_win
@ -42,7 +42,7 @@ from openlp.core.ui.style import UiThemes
log = logging.getLogger(__name__)
__version__ = 2
__version__ = 3
class ProxyMode(IntEnum):
@ -116,6 +116,16 @@ def upgrade_dark_theme_to_ui_theme(value):
return UiThemes.QDarkStyle if value else UiThemes.Automatic
def upgrade_add_first_songbook_slide_config(value):
"""
Upgrade the "songs/add songbook slide" property to "songs/add first slide".
:param bool value: the old "add_songbook_slide" value
:returns SongFirstSlideMode: new SongFirstSlideMode value
"""
return SongFirstSlideMode.Songbook if value is True else SongFirstSlideMode.Default
class Settings(QtCore.QSettings):
"""
Class to wrap QSettings.
@ -338,7 +348,7 @@ class Settings(QtCore.QSettings):
'songs/last import type': 0,
'songs/update service on edit': False,
'songs/add song from service': True,
'songs/add songbook slide': False,
'songs/first slide mode': SongFirstSlideMode.Default,
'songs/display songbar': True,
'songs/last directory import': None,
'songs/last directory export': None,
@ -472,6 +482,10 @@ class Settings(QtCore.QSettings):
('themes/last directory', 'themes/last directory', [(str_to_path, None)]),
('themes/wrap footer', '', []),
]
# Settings upgrades for 3.1
__setting_upgrade_3__ = [
('songs/add songbook slide', 'songs/first slide mode', [(upgrade_add_first_songbook_slide_config, False)])
]
@staticmethod
def extend_default_settings(default_values):

View File

@ -683,7 +683,13 @@ var Display = {
var footerSlide = document.createElement('div');
footerSlide.classList.add('footer-item');
footerSlide.setAttribute('data-slide', index);
if (index == 0) {
var currentSlide = Reveal.getIndices();
if (currentSlide) {
currentSlide = currentSlide.v;
} else {
currentSlide = 0;
}
if (index == currentSlide) {
footerSlide.classList.add('active');
}
footerSlide.innerHTML = slide.footer;

View File

@ -144,6 +144,22 @@ def remove_chords(text):
return _get_chord_match().sub(r'', text)
RE_HTML_STRIP = re.compile(r'<[^>]+>')
def remove_html_and_strip(text):
"""
Removes all HTML from the text and strips the whitespace from the remaining lines.
"""
lines = map(__clean_html_line, text.split('\n'))
return '\n'.join(lines)
def __clean_html_line(line):
line = RE_HTML_STRIP.sub('', line)
return line.strip()
def remove_tags(text, can_remove_chords=False):
"""
Remove Tags from text for display
@ -152,6 +168,8 @@ def remove_tags(text, can_remove_chords=False):
:param can_remove_chords: Can we remove the chords too?
"""
text = text.replace('<br>', '\n')
text = text.replace('<br/>', '\n')
text = text.replace('<br />', '\n')
text = text.replace('{br}', '\n')
text = text.replace('&nbsp;', ' ')
text = text.replace('<sup>', '')

View File

@ -39,7 +39,7 @@ from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.common.utils import wait_for
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
from openlp.core.display.render import remove_html_and_strip, remove_tags, render_tags, render_chords_for_printing
from openlp.core.lib import create_thumb, image_to_data_uri, ItemCapabilities
from openlp.core.lib.theme import BackgroundType, TransitionSpeed
from openlp.core.state import State
@ -237,13 +237,20 @@ class ServiceItem(RegistryProperties):
self._rendered_slides.append(rendered_slide)
display_slide = {
'title': raw_slide['title'],
'text': remove_tags(page, can_remove_chords=True),
'text': remove_html_and_strip(remove_tags(page, can_remove_chords=True)),
'verse': verse_tag,
}
self._display_slides.append(display_slide)
index += 1
self._creating_slides = False
def _clear_slides_cache(self):
"""
Clears the internal representation/cache of slides (display_slides and rendered_slides).
"""
self._display_slides = None
self._rendered_slides = None
@property
def rendered_slides(self):
"""
@ -309,13 +316,41 @@ class ServiceItem(RegistryProperties):
self.slides.append(slide)
self._new_item()
def add_from_text(self, text, verse_tag=None, footer_html=None):
def add_from_text(self, text, verse_tag=None, footer_html=None, metadata=None):
"""
Add a text slide to the service item.
:param text: The raw text of the slide.
:param verse_tag:
:param footer_html: Custom HTML footer for current slide
:param metadata: Additional metadata to add to service item
"""
slide = self._create_slide_from_text(text, verse_tag, footer_html, metadata)
self.slides.append(slide)
self._new_item()
def replace_slide_from_text(self, index, text, verse_tag=None, footer_html=None, metadata=None):
"""
Replace a text slide on the service item.
:param index: The index of slide to replace
:param text: The raw text of the slide.
:param verse_tag:
:param footer_html: Custom HTML footer for current slide
:param metadata: Additional metadata to add to service item
"""
slide = self._create_slide_from_text(text, verse_tag, footer_html, metadata)
self.slides[index] = slide
self._clear_slides_cache()
def _create_slide_from_text(self, text, verse_tag=None, footer_html=None, metadata=None):
"""
Creates a text slide.
:param text: The raw text of the slide.
:param verse_tag:
:param footer_html: Custom HTML footer for current slide
:param metadata: Additional metadata to add to service item
"""
if verse_tag:
verse_tag = verse_tag.upper()
@ -327,8 +362,9 @@ class ServiceItem(RegistryProperties):
slide = {'title': title, 'text': text, 'verse': verse_tag}
if footer_html is not None:
slide['footer_html'] = footer_html
self.slides.append(slide)
self._new_item()
if isinstance(metadata, dict):
slide['metadata'] = metadata
return slide
def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None):
"""
@ -517,7 +553,8 @@ class ServiceItem(RegistryProperties):
if self.service_item_type == ServiceItemType.Text:
for slide in service_item['serviceitem']['data']:
footer_html = slide['footer_html'] if 'footer_html' in slide else None
self.add_from_text(slide['raw_slide'], slide['verseTag'], footer_html)
metadata = slide['metadata'] if 'metadata' in slide and isinstance(slide['metadata'], dict) else None
self.add_from_text(slide['raw_slide'], slide['verseTag'], footer_html, metadata)
self._create_slides()
elif self.service_item_type == ServiceItemType.Image:
if path:

View File

@ -18,6 +18,7 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
from collections import namedtuple
import logging
import mako
import os
@ -28,7 +29,7 @@ from sqlalchemy.sql import and_, or_
from openlp.core.state import State
from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import SongSearch
from openlp.core.common.enum import SongFirstSlideMode, SongSearch
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
@ -565,19 +566,11 @@ class SongMediaItem(MediaManagerItem):
service_item.theme = song.theme_name
service_item.edit_id = item_id
verse_list = SongXML().get_verses(song.lyrics)
if self.settings.value('songs/add songbook slide') and song.songbook_entries:
first_slide = '\n'
for songbook_entry in song.songbook_entries:
if songbook_entry.entry:
first_slide += '{book} #{num}'.format(book=songbook_entry.songbook.name,
num=songbook_entry.entry)
else:
first_slide += songbook_entry.songbook.name
if songbook_entry.songbook.publisher:
first_slide += ' ({pub})'.format(pub=songbook_entry.songbook.publisher)
first_slide += '\n\n'
service_item.add_from_text(first_slide, 'O1')
authors = self._get_music_authors(song)
songbooks_str = [str(songbook_entry) for songbook_entry in song.songbook_entries]
mako_vars = self._get_mako_vars(song, authors, songbooks_str)
service_item.title = song.title
author_list = self.generate_first_slide_and_footer(service_item, song, authors, songbooks_str, mako_vars)
# no verse list or only 1 space (in error)
verse_tags_translated = False
if VerseType.from_translated_string(str(verse_list[0][0]['type'])) is not None:
@ -616,8 +609,6 @@ class SongMediaItem(MediaManagerItem):
force_verse = verse[1].split('[--}{--]\n')
for split_verse in force_verse:
service_item.add_from_text(split_verse, verse_def)
service_item.title = song.title
author_list = self.generate_footer(service_item, song)
service_item.data_string = {
'title': song.search_title,
'alternate_title': song.alternate_title,
@ -643,14 +634,48 @@ class SongMediaItem(MediaManagerItem):
service_item.will_auto_start = bool(self.settings.value('songs/auto play audio'))
return True
def generate_footer(self, item, song):
def generate_footer(self, item, song, authors, songbooks, mako_vars):
"""
Generates the song footer based on a song and adds details to a service item.
:param item: The service item to be amended
:param song: The song to be used to generate the footer
:param authors: The authors of the song
:return: List of all authors (only required for initial song generation)
"""
item.audit = [
song.title, authors.all, song.copyright, str(song.ccli_number)
]
item.raw_footer = []
item.raw_footer.append(song.title)
if authors.none:
item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
authors=create_separated_list(authors.none)))
if authors.words_music:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
authors=create_separated_list(authors.words_music)))
if authors.words:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Words],
authors=create_separated_list(authors.words)))
if authors.music:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Music],
authors=create_separated_list(authors.music)))
if authors.translation:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
authors=create_separated_list(authors.translation)))
if song.copyright:
item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
song=song.copyright))
if song.songbook_entries:
item.raw_footer.append(", ".join(songbooks))
if self.settings.value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
self.settings.value('core/ccli number'))
item.footer_html = self._generate_mako_footer(mako_vars)
return authors.all
def _get_music_authors(self, song):
authors_tuple = namedtuple('AuthorsTuple', ['words', 'music', 'words_music', 'translation', 'none', 'all'])
authors_words = []
authors_music = []
authors_words_music = []
@ -668,66 +693,45 @@ class SongMediaItem(MediaManagerItem):
else:
authors_none.append(author_song.author.display_name)
authors_all = authors_words_music + authors_words + authors_music + authors_translation + authors_none
item.audit = [
song.title, authors_all, song.copyright, str(song.ccli_number)
]
item.raw_footer = []
item.raw_footer.append(song.title)
if authors_none:
item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
authors=create_separated_list(authors_none)))
if authors_words_music:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
authors=create_separated_list(authors_words_music)))
if authors_words:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Words],
authors=create_separated_list(authors_words)))
if authors_music:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Music],
authors=create_separated_list(authors_music)))
if authors_translation:
item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
authors=create_separated_list(authors_translation)))
if song.copyright:
item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
song=song.copyright))
songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
if song.songbook_entries:
item.raw_footer.append(", ".join(songbooks))
if self.settings.value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
self.settings.value('core/ccli number'))
footer_template = self.settings.value('songs/footer template')
return authors_tuple(authors_words, authors_music, authors_words_music, authors_translation, authors_none,
authors_all)
def _get_mako_vars(self, song, authors, songbooks):
# Keep this in sync with the list in songstab.py
vars = {
return {
'title': song.title,
'alternate_title': song.alternate_title,
'authors_none_label': translate('OpenLP.Ui', 'Written by'),
'authors_none': authors_none,
'authors_none': authors.none,
'authors_words_label': AuthorType.Types[AuthorType.Words],
'authors_words': authors_words,
'authors_words': authors.words,
'authors_music_label': AuthorType.Types[AuthorType.Music],
'authors_music': authors_music,
'authors_music': authors.music,
'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic],
'authors_words_music': authors_words_music,
'authors_words_music': authors.words_music,
'authors_translation_label': AuthorType.Types[AuthorType.Translation],
'authors_translation': authors_translation,
'authors_words_all': authors_words + authors_words_music,
'authors_music_all': authors_music + authors_words_music,
'authors_translation': authors.translation,
'authors_words_all': authors.words + authors.words_music,
'authors_music_all': authors.music + authors.words_music,
'copyright': song.copyright,
'songbook_entries': songbooks,
'ccli_license': self.settings.value('core/ccli number'),
'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
'ccli_number': song.ccli_number,
'topics': [topic.name for topic in song.topics]
'topics': [topic.name for topic in song.topics],
'first_slide': False
}
def _generate_mako_footer(self, vars, show_error=True):
footer_template = self.settings.value('songs/footer template')
try:
item.footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
return mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
except mako.exceptions.SyntaxException:
log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
critical_error_message_box(message=translate('SongsPlugin.MediaItem',
'Failed to render Song footer html.\nSee log for details'))
return authors_all
if show_error:
critical_error_message_box(message=translate('SongsPlugin.MediaItem',
'Failed to render Song footer html.\nSee log for details'))
return None
def service_load(self, item):
"""
@ -765,10 +769,56 @@ class SongMediaItem(MediaManagerItem):
self._update_background_audio(song, item)
edit_id = song.id
# Update service with correct song id and return it to caller.
authors = self._get_music_authors(song)
songbooks_str = [str(songbook_entry) for songbook_entry in song.songbook_entries]
mako_vars = self._get_mako_vars(song, authors, songbooks_str)
self.generate_footer(item, song, authors, songbooks_str, mako_vars)
if len(item.slides):
first_slide = item.slides[0]
if 'metadata' in first_slide and 'songs_first_slide_type' in first_slide['metadata']:
try:
slide_mode = SongFirstSlideMode(first_slide['metadata']['songs_first_slide_type'])
if slide_mode == SongFirstSlideMode.Footer:
# For now only the footer needs to be regenerated on import, as it's dependent on what
# user defined on each OpenLP instance settings.
self.generate_first_slide_and_footer(item, song, authors, songbooks_str, mako_vars, True)
except ValueError:
# Maybe it's a new slide mode generated in a greater OpenLP version, better leave it as-is.
pass
item.edit_id = edit_id
self.generate_footer(item, song)
return item
def generate_first_slide_and_footer(self, service_item, song, authors, songbooks_str, mako_vars, replace=False):
song_first_slide = self.settings.value('songs/first slide mode')
service_item.title = song.title
author_list = self.generate_footer(service_item, song, authors, songbooks_str, mako_vars)
slide_metadata = {'songs_first_slide_type': song_first_slide}
if song_first_slide == SongFirstSlideMode.Songbook and song.songbook_entries:
first_slide = '\n'
for songbook_entry in song.songbook_entries:
if songbook_entry.entry:
first_slide += '{book} #{num}'.format(book=songbook_entry.songbook.name,
num=songbook_entry.entry)
else:
first_slide += songbook_entry.songbook.name
if songbook_entry.songbook.publisher:
first_slide += ' ({pub})'.format(pub=songbook_entry.songbook.publisher)
first_slide += '\n\n'
if replace:
service_item.replace_slide_from_text(0, first_slide, 'O1', metadata=slide_metadata)
else:
service_item.add_from_text(first_slide, 'O1', metadata=slide_metadata)
elif song_first_slide == SongFirstSlideMode.Footer:
mako_vars['first_slide'] = True
first_slide = self._generate_mako_footer(mako_vars, False) # Avoiding show message error box twice
first_slide = first_slide if first_slide is not None else '\n'.join(service_item.raw_footer)
if replace:
service_item.replace_slide_from_text(0, first_slide, 'O2', footer_html='', metadata=slide_metadata)
else:
service_item.add_from_text(first_slide, 'O2', footer_html='', metadata=slide_metadata)
mako_vars['first_slide'] = False
return author_list
@staticmethod
def _authors_match(song, authors):
"""

View File

@ -132,7 +132,7 @@ class SongXML(object):
if self.song_xml is not None:
xml_iter = self.song_xml.getiterator()
for element in xml_iter:
if element.tag == 'verse':
if etree.QName(element).localname == 'verse':
if element.text is None:
element.text = ''
verse_list.append([element.attrib, str(element.text)])

View File

@ -22,6 +22,7 @@
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.enum import SongFirstSlideMode
from openlp.core.lib.settingstab import SettingsTab
from openlp.plugins.songs.lib.db import AuthorType
@ -50,12 +51,20 @@ class SongsTab(SettingsTab):
self.add_from_service_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.add_from_service_check_box.setObjectName('add_from_service_check_box')
self.mode_layout.addWidget(self.add_from_service_check_box)
self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')
self.mode_layout.addWidget(self.songbook_slide_check_box)
self.auto_play_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.auto_play_check_box.setObjectName('auto_play_check_box')
self.mode_layout.addWidget(self.auto_play_check_box)
# First Slide Mode
self.first_slide_mode_widget = QtWidgets.QWidget(self.mode_group_box)
self.first_slide_mode_layout = QtWidgets.QHBoxLayout(self.first_slide_mode_widget)
self.first_slide_mode_layout.setContentsMargins(0, 0, 0, 0)
self.first_slide_mode_label = QtWidgets.QLabel(self.first_slide_mode_widget)
self.first_slide_mode_combobox = QtWidgets.QComboBox(self.first_slide_mode_widget)
self.first_slide_mode_combobox.addItems(['', '', ''])
self.first_slide_mode_layout.addWidget(self.first_slide_mode_label)
self.first_slide_mode_layout.addWidget(self.first_slide_mode_combobox)
self.first_slide_mode_widget.setLayout(self.first_slide_mode_layout)
self.mode_layout.addWidget(self.first_slide_mode_widget)
self.left_layout.addWidget(self.mode_group_box)
# Chords group box
@ -141,7 +150,7 @@ class SongsTab(SettingsTab):
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed)
self.first_slide_mode_combobox.currentIndexChanged.connect(self.on_first_slide_mode_combo_box_changed)
self.auto_play_check_box.stateChanged.connect(self.on_auto_play_check_box_changed)
self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
self.song_key_warning_check_box.stateChanged.connect(self.on_song_key_warning_check_box_changed)
@ -157,8 +166,10 @@ class SongsTab(SettingsTab):
self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab',
'Import missing songs from Service files'))
self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab',
'Add Songbooks as first slide'))
self.first_slide_mode_label.setText(translate('SongsPlugin.SongsTab', 'Add first slide:'))
self.first_slide_mode_combobox.setItemText(0, translate('SongsPlugin.SongsTab', 'None'))
self.first_slide_mode_combobox.setItemText(1, translate('SongsPlugin.SongsTab', 'Songbook'))
self.first_slide_mode_combobox.setItemText(2, translate('SongsPlugin.SongsTab', 'Same as Footer'))
self.auto_play_check_box.setText(translate('SongsPlugin.SongsTab', 'Auto-play background audio'))
self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
'be regarded as chords.'))
@ -201,6 +212,7 @@ class SongsTab(SettingsTab):
['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False],
['ccli_number', translate('SongsPlugin.SongsTab', 'Song CCLI Number'), True, False],
['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True],
['first_slide', translate('SongsPlugin.SongsTab', 'Where rendering on first (cover) slide'), False, False],
]
placeholder_info = '<table><tr><th><b>{ph}</b></th><th><b>{desc}</b></th></tr>'.format(
ph=translate('SongsPlugin.SongsTab', 'Placeholder'), desc=translate('SongsPlugin.SongsTab', 'Description'))
@ -233,8 +245,8 @@ class SongsTab(SettingsTab):
def on_add_from_service_check_box_changed(self, check_state):
self.update_load = (check_state == QtCore.Qt.Checked)
def on_songbook_slide_check_box_changed(self, check_state):
self.songbook_slide = (check_state == QtCore.Qt.Checked)
def on_first_slide_mode_combo_box_changed(self, index):
self.first_slide_mode = SongFirstSlideMode(index)
def on_auto_play_check_box_changed(self, check_state):
self.auto_play = (check_state == QtCore.Qt.Checked)
@ -264,7 +276,7 @@ class SongsTab(SettingsTab):
self.tool_bar = self.settings.value('songs/display songbar')
self.update_edit = self.settings.value('songs/update service on edit')
self.update_load = self.settings.value('songs/add song from service')
self.songbook_slide = self.settings.value('songs/add songbook slide')
self.first_slide_mode = self.settings.value('songs/first slide mode')
self.auto_play = self.settings.value('songs/auto play audio')
self.enable_chords = self.settings.value('songs/enable chords')
self.chord_notation = self.settings.value('songs/chord notation')
@ -286,6 +298,8 @@ class SongsTab(SettingsTab):
self.ccli_username.setText(self.settings.value('songs/songselect username'))
self.ccli_password.setText(self.settings.value('songs/songselect password'))
self.footer_edit_box.setPlainText(self.settings.value('songs/footer template'))
if self.first_slide_mode > 0:
self.first_slide_mode_combobox.setCurrentIndex(self.first_slide_mode)
def save(self):
"""
@ -315,7 +329,7 @@ class SongsTab(SettingsTab):
# Only save footer template if it has been changed. This allows future updates
if self.footer_edit_box.toPlainText() != self.settings.value('songs/footer template'):
self.settings.setValue('songs/footer template', self.footer_edit_box.toPlainText())
self.settings.setValue('songs/add songbook slide', self.songbook_slide)
self.settings.setValue('songs/first slide mode', self.first_slide_mode)
if self.tab_visited:
self.settings_form.register_post_process('songs_config_updated')
self.tab_visited = False

View File

@ -781,6 +781,8 @@ describe("Display.setTextSlide", function () {
});
describe("Display.setTextSlides", function () {
var textSlides;
beforeEach(function() {
document.body.innerHTML = "";
var slides_container = _createDiv({"class": "slides"});
@ -788,10 +790,7 @@ describe("Display.setTextSlides", function () {
Display._slidesContainer = slides_container;
Display._footerContainer = footer_container;
Display._slides = {};
});
it("should add a list of slides", function () {
var slides = [
textSlides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
@ -805,6 +804,10 @@ describe("Display.setTextSlides", function () {
"footer": "Public Domain"
}
];
});
it("should add a list of slides", function () {
var slides = textSlides;
spyOn(Display, "clearSlides");
spyOn(Reveal, "sync");
spyOn(Reveal, "slide");
@ -819,14 +822,7 @@ describe("Display.setTextSlides", function () {
});
it("should correctly set outline width", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const slides = [textSlides[0]];
const theme = {
'font_main_color': 'yellow',
'font_main_outline': true,
@ -845,14 +841,7 @@ describe("Display.setTextSlides", function () {
it("should correctly set text alignment,\
(check the order of alignments in the emuns are the same in both js and python)", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const slides = [textSlides[0]];
//
const theme = {
'display_horizontal_align': 3,
@ -870,14 +859,7 @@ describe("Display.setTextSlides", function () {
})
it("should enable shadows", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const slides = [textSlides[0]];
//
const theme = {
'font_main_shadow': true,
@ -895,14 +877,7 @@ describe("Display.setTextSlides", function () {
})
it("should not enable shadows", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const slides = [textSlides[0]];
//
const theme = {
'font_main_shadow': false,
@ -920,14 +895,7 @@ describe("Display.setTextSlides", function () {
})
it("should correctly set slide size position to theme size when adding a text slide", function () {
const slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
}
];
const slides = [textSlides[0]];
//
const theme = {
'font_main_y': 789,
@ -949,20 +917,7 @@ describe("Display.setTextSlides", function () {
})
it("should work correctly with different footer contents per slide", function () {
var slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
},
{
"verse": "v2",
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
"How precious did that grace appear,\nthe hour I first believed.",
"footer": "Public Domain, Second Test"
}
];
var slides = textSlides;
spyOn(Display, "clearSlides");
spyOn(Reveal, "sync");
spyOn(Reveal, "slide");
@ -974,6 +929,19 @@ describe("Display.setTextSlides", function () {
expect(document.querySelectorAll(".footer > .footer-item")[0].innerHTML).toEqual(slides[0].footer);
expect(document.querySelectorAll(".footer > .footer-item")[1].innerHTML).toEqual(slides[1].footer);
});
it("should select correct footer index when resetting text", function () {
var slides = textSlides;
slides[1]['footer'] = 'Public Domain, Second Test';
Display.init({isDisplay: false});
Display.setTextSlides(slides);
spyOn(Reveal, 'getIndices').and.returnValue({v: 1, h: 0});
slides[1]['footer'] = 'Second Slide';
Display.setTextSlides(slides);
expect(document.querySelectorAll(".footer > .footer-item")[1].classList.contains('active')).toBeTruthy();
});
});
describe("Display.setImageSlides", function () {
@ -1250,4 +1218,4 @@ describe("Reveal slidechanged event", function () {
currentSlide.id = '1';
Display._onSlideChanged({currentSlide: currentSlide});
});
});
});

View File

@ -985,10 +985,8 @@ def test_add_from_text_adds_per_slide_footer_html():
# THEN: Slides should be added with correctly numbered verse tags (Should start at 1)
assert service_item.slides == [
{'text': 'This is the first slide', 'title': 'This is the first slide', 'verse': '1',
'footer_html': slide1FooterHtml},
{'text': 'This is the second slide', 'title': 'This is the second slide', 'verse': '2',
'footer_html': slide2FooterHtml}
{'text': slide1, 'title': slide1, 'verse': '1', 'footer_html': slide1FooterHtml},
{'text': slide2, 'title': slide2, 'verse': '2', 'footer_html': slide2FooterHtml}
]

View File

@ -26,10 +26,13 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtCore
from openlp.core.common.enum import SongFirstSlideMode
from openlp.core.common.registry import Registry
from openlp.core.lib.serviceitem import ServiceItem
from openlp.plugins.songs.lib.db import AuthorType, Song
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics
__default_settings__ = {
'songs/footer template': """
@ -88,6 +91,10 @@ ${title}<br/>
}
SONG_VERSES_TEST_LYRICS = [[{'type': 'v', 'label': '1'}, 'Test text']]
SONG_VERSES_TEST_VERSE_ORDER = 'v1'
@pytest.fixture
def media_item(settings):
Registry().register('service_list', MagicMock())
@ -370,9 +377,12 @@ def test_build_song_footer_two_authors(media_item):
mock_song.copyright = 'My copyright'
mock_song.songbook_entries = []
service_item = ServiceItem(None)
songbooks_str = []
authors = media_item._get_music_authors(mock_song)
mako_vars = media_item._get_mako_vars(mock_song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
author_list = media_item.generate_footer(service_item, mock_song)
author_list = media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: I get the following Array returned
assert service_item.raw_footer == ['My Song', 'Words: another author', 'Music: my author',
@ -393,9 +403,12 @@ def test_build_song_footer_base_ccli(media_item):
mock_song.songbook_entries = []
service_item = ServiceItem(None)
media_item.settings.setValue('core/ccli number', '1234')
songbooks_str = []
authors = media_item._get_music_authors(mock_song)
mako_vars = media_item._get_mako_vars(mock_song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
media_item.generate_footer(service_item, mock_song)
media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: I get the following Array returned
assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 1234'], \
@ -403,7 +416,7 @@ def test_build_song_footer_base_ccli(media_item):
# WHEN: I amend the CCLI value
media_item.settings.setValue('core/ccli number', '4321')
media_item.generate_footer(service_item, mock_song)
media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: I would get an amended footer string
assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 4321'], \
@ -428,13 +441,16 @@ def test_build_song_footer_base_songbook(media_item):
book1.name = 'My songbook'
book2 = MagicMock()
book2.name = 'Thy songbook'
song.songbookentries = []
song.songbook_entries = []
song.add_songbook_entry(book1, '12')
song.add_songbook_entry(book2, '502A')
service_item = ServiceItem(None)
songbooks_str = [str(songbook) for songbook in song.songbook_entries]
authors = media_item._get_music_authors(song)
mako_vars = media_item._get_mako_vars(song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
media_item.generate_footer(service_item, song)
media_item.generate_footer(service_item, song, authors, songbooks_str, mako_vars)
# THEN: The songbook should be in the footer
assert service_item.raw_footer == ['My Song', '© My copyright', 'My songbook #12, Thy songbook #502A']
@ -451,9 +467,12 @@ def test_build_song_footer_copyright_enabled(media_item):
mock_song.copyright = 'My copyright'
mock_song.songbook_entries = []
service_item = ServiceItem(None)
songbooks_str = []
authors = media_item._get_music_authors(mock_song)
mako_vars = media_item._get_mako_vars(mock_song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
media_item.generate_footer(service_item, mock_song)
media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: The copyright symbol should be in the footer
assert service_item.raw_footer == ['My Song', '© My copyright']
@ -469,9 +488,12 @@ def test_build_song_footer_copyright_disabled(media_item):
mock_song.copyright = 'My copyright'
mock_song.songbook_entries = []
service_item = ServiceItem(None)
songbooks_str = []
authors = media_item._get_music_authors(mock_song)
mako_vars = media_item._get_mako_vars(mock_song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
media_item.generate_footer(service_item, mock_song)
media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: The copyright symbol should not be in the footer
assert service_item.raw_footer == ['My Song', '© My copyright']
@ -601,9 +623,12 @@ def test_build_song_footer_one_author_show_written_by(media_item):
mock_song.copyright = 'My copyright'
mock_song.songbook_entries = []
service_item = ServiceItem(None)
songbooks_str = []
authors = media_item._get_music_authors(mock_song)
mako_vars = media_item._get_mako_vars(mock_song, authors, songbooks_str)
# WHEN: I generate the Footer with default settings
author_list = media_item.generate_footer(service_item, mock_song)
author_list = media_item.generate_footer(service_item, mock_song, authors, songbooks_str, mako_vars)
# THEN: The mako function was called with the following arguments
args = {'authors_translation': [], 'authors_music_label': 'Music',
@ -616,6 +641,66 @@ def test_build_song_footer_one_author_show_written_by(media_item):
'authors_none': ['my author'],
'ccli_license_label': 'CCLI License', 'authors_words': [],
'ccli_license': '0', 'authors_translation_label': 'Translation',
'authors_words_all': []}
'authors_words_all': [], 'first_slide': False}
MockedRenderer.assert_called_once_with(**args)
assert author_list == ['my author'], 'The author list should be returned correctly with one author'
@patch('openlp.plugins.songs.lib.mediaitem.SongMediaItem._get_id_of_item_to_generate')
@patch('openlp.plugins.songs.lib.mediaitem.SongXML.get_verses')
@pytest.mark.parametrize('first_slide_mode', SongFirstSlideMode)
def test_song_first_slide_creation_works(mocked_get_verses, mocked__get_id_of_item_to_generate, media_item,
first_slide_mode, settings):
"""
Test building song with SongFirstSlideMode = Songbook works
"""
# GIVEN: A Song and a Service Item
mocked__get_id_of_item_to_generate.return_value = '00000000-0000-0000-0000-000000000000'
settings.setValue('songs/first slide mode', first_slide_mode)
mocked_get_verses.return_value = SONG_VERSES_TEST_LYRICS
media_item.plugin = MagicMock()
media_item.open_lyrics = OpenLyrics(media_item.plugin.manager)
song = Song()
song.title = 'My Song'
song.alternate_title = ''
song.copyright = 'My copyright'
song.authors_songs = []
song.songbook_entries = []
song.alternate_title = ''
song.lyrics = 'Teste'
song.theme_name = 'Default'
song.topics = []
song.ccli_number = ''
song.lyrics = '<fake xml>' # Mocked by mocked_get_verses
song.verse_order = SONG_VERSES_TEST_VERSE_ORDER
song.search_title = 'my song@'
song.last_modified = '2023-02-20T00:00:00Z'
song.media_files = []
song.comments = ''
book1 = MagicMock()
book1.name = 'My songbook'
book1.publisher = None
book2 = MagicMock()
book2.name = 'Thy songbook'
book2.publisher = 'Publisher'
song.songbook_entries = []
song.add_songbook_entry(book1, '12')
song.add_songbook_entry(book2, '502A')
service_item = ServiceItem(None)
media_item.plugin.manager.get_object.return_value = song
# WHEN: I generate the Footer with default settings
media_item.generate_slide_data(service_item, item=song)
# THEN: The copyright symbol should not be in the footer
if first_slide_mode == SongFirstSlideMode.Default:
# No metadata is needed on default slide mode (at least for now)
assert 'metadata' not in service_item.slides[0]
else:
assert service_item.slides[0]['metadata']['songs_first_slide_type'] == first_slide_mode
if first_slide_mode == SongFirstSlideMode.Songbook:
assert service_item.slides[0]['text'] == '\nMy songbook #12\n\nThy songbook #502A (Publisher)\n\n'
if first_slide_mode == SongFirstSlideMode.Footer:
assert service_item.slides[0]['text'] == service_item.footer_html
# It needs to have empty footer as it's already shown on text
assert service_item.slides[0]['footer_html'] == ''

View File

@ -25,6 +25,7 @@ import pytest
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.enum import SongFirstSlideMode
from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib.songstab import SongsTab
@ -89,7 +90,6 @@ def test_save_check_box_settings(form):
form.on_tool_bar_active_check_box_changed(QtCore.Qt.Checked)
form.on_update_on_edit_check_box_changed(QtCore.Qt.Unchecked)
form.on_add_from_service_check_box_changed(QtCore.Qt.Checked)
form.on_songbook_slide_check_box_changed(QtCore.Qt.Unchecked)
form.on_disable_chords_import_check_box_changed(QtCore.Qt.Unchecked)
form.on_auto_play_check_box_changed(QtCore.Qt.Checked)
# WHEN: Save is invoked
@ -99,7 +99,6 @@ def test_save_check_box_settings(form):
assert form.settings.value('songs/display songbar') is True
assert form.settings.value('songs/update service on edit') is False
assert form.settings.value('songs/add song from service') is True
assert form.settings.value('songs/add songbook slide') is False
assert form.settings.value('songs/disable chords import') is False
assert form.settings.value('songs/auto play audio') is True
@ -245,3 +244,17 @@ def test_save_tab_change(form):
# THEN: the post process should be requested
assert 1 == form.settings_form.register_post_process.call_count, \
'Songs Post processing should have been requested'
def test_save_first_slide_settings(form):
"""
Tests that "Add First Slide" setting is saved correctly.
"""
# GIVEN: "Add First Slide" has been changed
form.on_first_slide_mode_combo_box_changed(2)
# WHEN: save() is invoked
form.save()
# THEN: The correct values should be stored in the settings
assert form.settings.value('songs/first slide mode') is SongFirstSlideMode.Footer.value