Merge lp:~sam92/openlp/bug-1695620 into lp:openlp

Proposed by Samuel Mehrbrodt
Status: Superseded
Proposed branch: lp:~sam92/openlp/bug-1695620
Merge into: lp:openlp
Diff against target: 724 lines (+284/-155)
6 files modified
openlp/core/lib/serviceitem.py (+5/-3)
openlp/core/ui/printserviceform.py (+5/-5)
openlp/plugins/songs/lib/mediaitem.py (+40/-36)
openlp/plugins/songs/lib/songstab.py (+72/-37)
openlp/plugins/songs/songsplugin.py (+55/-5)
tests/functional/openlp_plugins/songs/test_mediaitem.py (+107/-69)
To merge this branch: bzr merge lp:~sam92/openlp/bug-1695620
Reviewer Review Type Date Requested Status
Tomas Groth Pending
Tim Bentley Pending
Raoul Snyman Pending
Review via email: mp+362915@code.launchpad.net

This proposal supersedes a proposal from 2017-06-13.

This proposal has been superseded by a proposal from 2019-02-21.

Commit message

Make use of Mako for footer generation being configurable in song settings

Description of the change

Make use of Mako for footer generation being configurable in song settings

- removed now obsolete and via template better configurable options to display "songbook", "written by" and "copyright" information in footer
- added explanation box for so far used settings as Mako placeholders
- added songs configuration setting for template including reset button
- added default template replacing currently existing configuration as best as possible (should be backwards compatible or at least be adaptable to correspond to former settings)
- write and adapt tests for new and removed functionality
- Added some more available fields in the footer: Alternate Title, CCLI Number, Topics, Authors (all music, all words)

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

You need to run CI on this to show the success of tests.
Not sure why but this request did a full download when I did a check out. Also cannot run due to chord issues.
Cannot run this but there seem to be lots of line breaks in the footer display. As the footer area is small this will not be displayed and lead to forum comments.
You need some validation do defend against too long footers!

review: Needs Fixing
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

We're moving away from Python generated HTML, and using a JS system based on Reveal.js. Come chat with tgc and myself in IRC.

review: Disapprove
Revision history for this message
Samuel Mehrbrodt (sam92) wrote : Posted in a previous version of this proposal

> Cannot run this but there seem to be lots of line breaks in the footer display. As the footer area is small this will not be displayed and lead to forum comments.

The footer has the same height as before, there should be no difference by default.

So I replaced Pystache with Mako. Should be safe to merge now I think.
As the new renderer is in a very early state atm, I can't see how I can adopt this for the new renderer. Maybe we can merge this now and adopt when the new renderer is ready?

Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

Blank lines!
Will review tomorrow as I have bible study!

review: Needs Fixing
Revision history for this message
Samuel Mehrbrodt (sam92) wrote : Posted in a previous version of this proposal

So is there a chance this can be reviewed now?

> Blank lines!
What do you mean with that?

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linux tests passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Linting passed!

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

macOS tests passed!

lp:~sam92/openlp/bug-1695620 updated
2762. By Samuel Mehrbrodt

Merge trunk

2763. By Samuel Mehrbrodt

Merge trunk

2764. By Samuel Mehrbrodt

Fix songstab

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py 2019-02-14 15:09:09 +0000
+++ openlp/core/lib/serviceitem.py 2019-02-21 21:27:51 +0000
@@ -81,7 +81,8 @@
81 self.items = []81 self.items = []
82 self.icon = UiIcons().default82 self.icon = UiIcons().default
83 self.raw_footer = []83 self.raw_footer = []
84 self.foot_text = ''84 # Plugins can set footer_html themselves. If they don't, it will be generated from raw_footer.
85 self.footer_html = ''
85 self.theme = None86 self.theme = None
86 self.service_item_type = None87 self.service_item_type = None
87 self.unique_identifier = 088 self.unique_identifier = 0
@@ -165,7 +166,8 @@
165 # the dict instead of rendering them again.166 # the dict instead of rendering them again.
166 previous_pages = {}167 previous_pages = {}
167 index = 0168 index = 0
168 self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])169 if not self.footer_html:
170 self.footer_html = '<br>'.join([_f for _f in self.raw_footer if _f])
169 for raw_slide in self.slides:171 for raw_slide in self.slides:
170 verse_tag = raw_slide['verse']172 verse_tag = raw_slide['verse']
171 if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:173 if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
@@ -178,7 +180,7 @@
178 'title': raw_slide['title'],180 'title': raw_slide['title'],
179 'text': render_tags(page),181 'text': render_tags(page),
180 'verse': index,182 'verse': index,
181 'footer': self.foot_text,183 'footer': self.footer_html,
182 }184 }
183 self._rendered_slides.append(rendered_slide)185 self._rendered_slides.append(rendered_slide)
184 display_slide = {186 display_slide = {
185187
=== modified file 'openlp/core/ui/printserviceform.py'
--- openlp/core/ui/printserviceform.py 2019-02-14 15:09:09 +0000
+++ openlp/core/ui/printserviceform.py 2019-02-21 21:27:51 +0000
@@ -235,11 +235,11 @@
235 for slide in range(len(item.get_frames())):235 for slide in range(len(item.get_frames())):
236 self._add_element('li', item.get_frame_title(slide), ol)236 self._add_element('li', item.get_frame_title(slide), ol)
237 # add footer237 # add footer
238 foot_text = item.foot_text238 footer_html = item.footer_html
239 foot_text = foot_text.partition('<br>')[2]239 footer_html = footer_html.partition('<br>')[2]
240 if foot_text:240 if footer_html:
241 foot_text = html.escape(foot_text.replace('<br>', '\n'))241 footer_html = html.escape(footer_html.replace('<br>', '\n'))
242 self._add_element('div', foot_text.replace('\n', '<br>'), parent=div, class_id='itemFooter')242 self._add_element('div', footer_html.replace('\n', '<br>'), parent=div, classId='itemFooter')
243 # Add service items' notes.243 # Add service items' notes.
244 if self.notes_check_box.isChecked():244 if self.notes_check_box.isChecked():
245 if item.notes:245 if item.notes:
246246
=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py 2019-02-14 15:09:09 +0000
+++ openlp/plugins/songs/lib/mediaitem.py 2019-02-21 21:27:51 +0000
@@ -21,6 +21,7 @@
21###############################################################################21###############################################################################
22import logging22import logging
23import os23import os
24import mako
2425
25from PyQt5 import QtCore, QtWidgets26from PyQt5 import QtCore, QtWidgets
26from sqlalchemy.sql import and_, or_27from sqlalchemy.sql import and_, or_
@@ -35,7 +36,7 @@
35from openlp.core.lib.mediamanageritem import MediaManagerItem36from openlp.core.lib.mediamanageritem import MediaManagerItem
36from openlp.core.lib.plugin import PluginStatus37from openlp.core.lib.plugin import PluginStatus
37from openlp.core.lib.serviceitem import ItemCapabilities38from openlp.core.lib.serviceitem import ItemCapabilities
38from openlp.core.lib.ui import create_widget_action39from openlp.core.lib.ui import create_widget_action, critical_error_message_box
39from openlp.core.ui.icons import UiIcons40from openlp.core.ui.icons import UiIcons
40from openlp.plugins.songs.forms.editsongform import EditSongForm41from openlp.plugins.songs.forms.editsongform import EditSongForm
41from openlp.plugins.songs.forms.songexportform import SongExportForm42from openlp.plugins.songs.forms.songexportform import SongExportForm
@@ -131,9 +132,6 @@
131 self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')132 self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
132 self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')133 self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
133 self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')134 self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
134 self.display_songbook = Settings().value(self.settings_section + '/display songbook')
135 self.display_written_by_text = Settings().value(self.settings_section + '/display written by')
136 self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol')
137135
138 def retranslate_ui(self):136 def retranslate_ui(self):
139 self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))137 self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
@@ -677,12 +675,8 @@
677 item.raw_footer = []675 item.raw_footer = []
678 item.raw_footer.append(song.title)676 item.raw_footer.append(song.title)
679 if authors_none:677 if authors_none:
680 # If the setting for showing "Written by:" is enabled, show it before unspecified authors.678 item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
681 if Settings().value('songs/display written by'):679 authors=create_separated_list(authors_none)))
682 item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
683 authors=create_separated_list(authors_none)))
684 else:
685 item.raw_footer.append("{authors}".format(authors=create_separated_list(authors_none)))
686 if authors_words_music:680 if authors_words_music:
687 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],681 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
688 authors=create_separated_list(authors_words_music)))682 authors=create_separated_list(authors_words_music)))
@@ -696,34 +690,44 @@
696 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],690 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
697 authors=create_separated_list(authors_translation)))691 authors=create_separated_list(authors_translation)))
698 if song.copyright:692 if song.copyright:
699 if self.display_copyright_symbol:693 item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
700 item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,694 song=song.copyright))
701 song=song.copyright))695 songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
702 else:696 if song.songbook_entries:
703 item.raw_footer.append(song.copyright)
704 if self.display_songbook and song.songbook_entries:
705 songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
706 item.raw_footer.append(", ".join(songbooks))697 item.raw_footer.append(", ".join(songbooks))
707 if Settings().value('core/ccli number'):698 if Settings().value('core/ccli number'):
708 item.raw_footer.append(translate('SongsPlugin.MediaItem',699 item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
709 'CCLI License: ') + Settings().value('core/ccli number'))700 Settings().value('core/ccli number'))
710 item.metadata.append('<em>{label}:</em> {title}'.format(label=translate('SongsPlugin.MediaItem', 'Title'),701 footer_template = Settings().value('songs/footer template')
711 title=song.title))702 # Keep this in sync with the list in songstab.py
712 if song.alternate_title:703 vars = {
713 item.metadata.append('<em>{label}:</em> {title}'.704 'title': song.title,
714 format(label=translate('SongsPlugin.MediaItem', 'Alt Title'),705 'alternate_title': song.alternate_title,
715 title=song.alternate_title))706 'authors_none_label': translate('OpenLP.Ui', 'Written by'),
716 if song.songbook_entries:707 'authors_none': authors_none,
717 for songbook_entry in song.songbook_entries:708 'authors_words_label': AuthorType.Types[AuthorType.Words],
718 item.metadata.append('<em>{label}:</em> {book}/{num}/{pub}'.709 'authors_words': authors_words,
719 format(label=translate('SongsPlugin.MediaItem', 'Songbook'),710 'authors_music_label': AuthorType.Types[AuthorType.Music],
720 book=songbook_entry.songbook.name,711 'authors_music': authors_music,
721 num=songbook_entry.entry,712 'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic],
722 pub=songbook_entry.songbook.publisher))713 'authors_words_music': authors_words_music,
723 if song.topics:714 'authors_translation_label': AuthorType.Types[AuthorType.Translation],
724 for topics in song.topics:715 'authors_translation': authors_translation,
725 item.metadata.append('<em>{label}:</em> {topic}'.716 'authors_words_all': authors_words + authors_words_music,
726 format(label=translate('SongsPlugin.MediaItem', 'Topic'), topic=topics.name))717 'authors_music_all': authors_music + authors_words_music,
718 'copyright': song.copyright,
719 'songbook_entries': songbooks,
720 'ccli_license': Settings().value('core/ccli number'),
721 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
722 'ccli_number': song.ccli_number,
723 'topics': [topic.name for topic in song.topics]
724 }
725 try:
726 item.footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
727 except mako.exceptions.SyntaxException:
728 log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
729 critical_error_message_box(message=translate('SongsPlugin.MediaItem',
730 'Failed to render Song footer html.\nSee log for details'))
727 return authors_all731 return authors_all
728732
729 def service_load(self, item):733 def service_load(self, item):
730734
=== modified file 'openlp/plugins/songs/lib/songstab.py'
--- openlp/plugins/songs/lib/songstab.py 2019-02-14 15:09:09 +0000
+++ openlp/plugins/songs/lib/songstab.py 2019-02-21 21:27:51 +0000
@@ -25,7 +25,7 @@
25from openlp.core.common.i18n import translate25from openlp.core.common.i18n import translate
26from openlp.core.common.settings import Settings26from openlp.core.common.settings import Settings
27from openlp.core.lib.settingstab import SettingsTab27from openlp.core.lib.settingstab import SettingsTab
28from openlp.plugins.songs.lib.ui import SongStrings28from openlp.plugins.songs.lib.db import AuthorType
2929
3030
31class SongsTab(SettingsTab):31class SongsTab(SettingsTab):
@@ -54,15 +54,6 @@
54 self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)54 self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)
55 self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')55 self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')
56 self.mode_layout.addWidget(self.songbook_slide_check_box)56 self.mode_layout.addWidget(self.songbook_slide_check_box)
57 self.display_songbook_check_box = QtWidgets.QCheckBox(self.mode_group_box)
58 self.display_songbook_check_box.setObjectName('songbook_check_box')
59 self.mode_layout.addWidget(self.display_songbook_check_box)
60 self.display_written_by_check_box = QtWidgets.QCheckBox(self.mode_group_box)
61 self.display_written_by_check_box.setObjectName('written_by_check_box')
62 self.mode_layout.addWidget(self.display_written_by_check_box)
63 self.display_copyright_check_box = QtWidgets.QCheckBox(self.mode_group_box)
64 self.display_copyright_check_box.setObjectName('copyright_check_box')
65 self.mode_layout.addWidget(self.display_copyright_check_box)
66 self.left_layout.addWidget(self.mode_group_box)57 self.left_layout.addWidget(self.mode_group_box)
67 # Chords group box58 # Chords group box
68 self.chords_group_box = QtWidgets.QGroupBox(self.left_column)59 self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
@@ -93,20 +84,34 @@
93 self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')84 self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
94 self.chords_layout.addWidget(self.neolatin_notation_radio_button)85 self.chords_layout.addWidget(self.neolatin_notation_radio_button)
95 self.left_layout.addWidget(self.chords_group_box)86 self.left_layout.addWidget(self.chords_group_box)
87 # Footer group box
88 self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
89 self.footer_group_box.setObjectName('footer_group_box')
90 self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box)
91 self.footer_layout.setObjectName('chords_layout')
92 self.footer_info_label = QtWidgets.QLabel(self.footer_group_box)
93 self.footer_layout.addWidget(self.footer_info_label)
94 self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box)
95 self.footer_layout.addWidget(self.footer_placeholder_info)
96 self.footer_desc_label = QtWidgets.QLabel(self.footer_group_box)
97 self.footer_layout.addWidget(self.footer_desc_label)
98 self.footer_edit_box = QtWidgets.QTextEdit(self.footer_group_box)
99 self.footer_layout.addWidget(self.footer_edit_box)
100 self.footer_reset_button = QtWidgets.QPushButton(self.footer_group_box)
101 self.footer_layout.addWidget(self.footer_reset_button, alignment=QtCore.Qt.AlignRight)
102 self.right_layout.addWidget(self.footer_group_box)
96 self.left_layout.addStretch()103 self.left_layout.addStretch()
97 self.right_layout.addStretch()104 self.right_layout.addStretch()
98 self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)105 self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
99 self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)106 self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
100 self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)107 self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
101 self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed)108 self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed)
102 self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
103 self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
104 self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
105 self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)109 self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
106 self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)110 self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
107 self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)111 self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
108 self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)112 self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)
109 self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)113 self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)
114 self.footer_reset_button.clicked.connect(self.on_footer_reset_button_clicked)
110115
111 def retranslate_ui(self):116 def retranslate_ui(self):
112 self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))117 self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
@@ -117,12 +122,6 @@
117 'Import missing songs from Service files'))122 'Import missing songs from Service files'))
118 self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab',123 self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab',
119 'Add Songbooks as first side'))124 'Add Songbooks as first side'))
120 self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
121 self.display_written_by_check_box.setText(translate(
122 'SongsPlugin.SongsTab', 'Show "Written by:" in footer for unspecified authors'))
123 self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
124 'Display "{symbol}" symbol before copyright '
125 'info').format(symbol=SongStrings.CopyrightSymbol))
126 self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '125 self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
127 'be regarded as chords.'))126 'be regarded as chords.'))
128 self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))127 self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
@@ -134,6 +133,53 @@
134 self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')133 self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
135 self.neolatin_notation_radio_button.setText(134 self.neolatin_notation_radio_button.setText(
136 translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')135 translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')
136 self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer'))
137 # Keep this in sync with the list in mediaitem.py
138 const = '<code>"{}"</code>'
139 placeholders = [
140 # placeholder, description, can be empty, is a list
141 ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False],
142 ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), True, False],
143 ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False],
144 ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), False, True],
145 ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False],
146 ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type "Words")'), False, True],
147 ['authors_music_label', const.format(AuthorType.Types[AuthorType.Music]), False, False],
148 ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Music")'), False, True],
149 ['authors_words_music_label', const.format(AuthorType.Types[AuthorType.WordsAndMusic]), False, False],
150 ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Words and Music")'), False, True],
151 ['authors_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False],
152 ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type "Translation")'), False, True],
153 ['authors_words_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Words" & "Words and Music")'),
154 False, True],
155 ['authors_music_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Music" & "Words and Music")'),
156 False, True],
157 ['copyright', translate('SongsPlugin.SongsTab', 'Copyright information'), True, False],
158 ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), False, True],
159 ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False],
160 ['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False],
161 ['ccli_number', translate('SongsPlugin.SongsTab', 'Song CCLI Number'), True, False],
162 ['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True],
163 ]
164 placeholder_info = '<table style="background: #eee">\n<tr><th><b>{ph}</b></th><th><b>{desc}</b></th></tr>\n'\
165 .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'),
166 desc=translate('SongsPlugin.SongsTab', 'Description'))
167 for placeholder in placeholders:
168 placeholder_info += '<tr><td>${{{pl}}}</td><td>{des}{opt}</td></tr>\n'\
169 .format(pl=placeholder[0], des=placeholder[1],
170 opt=('&nbsp;¹' if placeholder[2] else '') +
171 ('&nbsp;²' if placeholder[3] else ''))
172 placeholder_info += '</table>'
173 placeholder_info += '\n<br/>¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty'))
174 placeholder_info += '\n<br/>² {}'.format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty'))
175 self.footer_placeholder_info.setHtml(placeholder_info)
176 self.footer_placeholder_info.setReadOnly(True)
177
178 self.footer_info_label.setText(translate('SongsPlugin.SongsTab', 'How to use Footers:'))
179 self.footer_desc_label.setText('{} (<a href="http://docs.makotemplates.org">{}</a>):'
180 .format(translate('SongsPlugin.SongsTab', 'Footer Template'),
181 translate('SongsPlugin.SongsTab', 'Mako Syntax')))
182 self.footer_reset_button.setText(translate('SongsPlugin.SongsTab', 'Reset Template'))
137183
138 def on_search_as_type_check_box_changed(self, check_state):184 def on_search_as_type_check_box_changed(self, check_state):
139 self.song_search = (check_state == QtCore.Qt.Checked)185 self.song_search = (check_state == QtCore.Qt.Checked)
@@ -150,15 +196,6 @@
150 def on_songbook_slide_check_box_changed(self, check_state):196 def on_songbook_slide_check_box_changed(self, check_state):
151 self.songbook_slide = (check_state == QtCore.Qt.Checked)197 self.songbook_slide = (check_state == QtCore.Qt.Checked)
152198
153 def on_songbook_check_box_changed(self, check_state):
154 self.display_songbook = (check_state == QtCore.Qt.Checked)
155
156 def on_written_by_check_box_changed(self, check_state):
157 self.display_written_by = (check_state == QtCore.Qt.Checked)
158
159 def on_copyright_check_box_changed(self, check_state):
160 self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
161
162 def on_mainview_chords_check_box_changed(self, check_state):199 def on_mainview_chords_check_box_changed(self, check_state):
163 self.mainview_chords = (check_state == QtCore.Qt.Checked)200 self.mainview_chords = (check_state == QtCore.Qt.Checked)
164201
@@ -174,6 +211,9 @@
174 def on_neolatin_notation_button_clicked(self):211 def on_neolatin_notation_button_clicked(self):
175 self.chord_notation = 'neo-latin'212 self.chord_notation = 'neo-latin'
176213
214 def on_footer_reset_button_clicked(self):
215 self.footer_edit_box.setPlainText(Settings().get_default_value('songs/footer template'))
216
177 def load(self):217 def load(self):
178 settings = Settings()218 settings = Settings()
179 settings.beginGroup(self.settings_section)219 settings.beginGroup(self.settings_section)
@@ -181,9 +221,6 @@
181 self.update_edit = settings.value('update service on edit')221 self.update_edit = settings.value('update service on edit')
182 self.update_load = settings.value('add song from service')222 self.update_load = settings.value('add song from service')
183 self.songbook_slide = settings.value('add songbook slide')223 self.songbook_slide = settings.value('add songbook slide')
184 self.display_songbook = settings.value('display songbook')
185 self.display_written_by = settings.value('display written by')
186 self.display_copyright_symbol = settings.value('display copyright symbol')
187 self.enable_chords = settings.value('enable chords')224 self.enable_chords = settings.value('enable chords')
188 self.chord_notation = settings.value('chord notation')225 self.chord_notation = settings.value('chord notation')
189 self.mainview_chords = settings.value('mainview chords')226 self.mainview_chords = settings.value('mainview chords')
@@ -191,9 +228,6 @@
191 self.tool_bar_active_check_box.setChecked(self.tool_bar)228 self.tool_bar_active_check_box.setChecked(self.tool_bar)
192 self.update_on_edit_check_box.setChecked(self.update_edit)229 self.update_on_edit_check_box.setChecked(self.update_edit)
193 self.add_from_service_check_box.setChecked(self.update_load)230 self.add_from_service_check_box.setChecked(self.update_load)
194 self.display_songbook_check_box.setChecked(self.display_songbook)
195 self.display_written_by_check_box.setChecked(self.display_written_by)
196 self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
197 self.chords_group_box.setChecked(self.enable_chords)231 self.chords_group_box.setChecked(self.enable_chords)
198 self.mainview_chords_check_box.setChecked(self.mainview_chords)232 self.mainview_chords_check_box.setChecked(self.mainview_chords)
199 self.disable_chords_import_check_box.setChecked(self.disable_chords_import)233 self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
@@ -203,6 +237,7 @@
203 self.neolatin_notation_radio_button.setChecked(True)237 self.neolatin_notation_radio_button.setChecked(True)
204 else:238 else:
205 self.english_notation_radio_button.setChecked(True)239 self.english_notation_radio_button.setChecked(True)
240 self.footer_edit_box.setPlainText(settings.value('footer template'))
206 settings.endGroup()241 settings.endGroup()
207242
208 def save(self):243 def save(self):
@@ -211,13 +246,13 @@
211 settings.setValue('display songbar', self.tool_bar)246 settings.setValue('display songbar', self.tool_bar)
212 settings.setValue('update service on edit', self.update_edit)247 settings.setValue('update service on edit', self.update_edit)
213 settings.setValue('add song from service', self.update_load)248 settings.setValue('add song from service', self.update_load)
214 settings.setValue('display songbook', self.display_songbook)
215 settings.setValue('display written by', self.display_written_by)
216 settings.setValue('display copyright symbol', self.display_copyright_symbol)
217 settings.setValue('enable chords', self.chords_group_box.isChecked())249 settings.setValue('enable chords', self.chords_group_box.isChecked())
218 settings.setValue('mainview chords', self.mainview_chords)250 settings.setValue('mainview chords', self.mainview_chords)
219 settings.setValue('disable chords import', self.disable_chords_import)251 settings.setValue('disable chords import', self.disable_chords_import)
220 settings.setValue('chord notation', self.chord_notation)252 settings.setValue('chord notation', self.chord_notation)
253 # Only save footer template if it has been changed. This allows future updates
254 if self.footer_edit_box.toPlainText() != Settings().get_default_value('songs/footer template'):
255 settings.setValue('footer template', self.footer_edit_box.toPlainText())
221 settings.setValue('add songbook slide', self.songbook_slide)256 settings.setValue('add songbook slide', self.songbook_slide)
222 settings.endGroup()257 settings.endGroup()
223 if self.tab_visited:258 if self.tab_visited:
224259
=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py 2019-02-14 15:09:09 +0000
+++ openlp/plugins/songs/songsplugin.py 2019-02-21 21:27:51 +0000
@@ -66,11 +66,8 @@
66 'songs/add song from service': True,66 'songs/add song from service': True,
67 'songs/add songbook slide': False,67 'songs/add songbook slide': False,
68 'songs/display songbar': True,68 'songs/display songbar': True,
69 'songs/display songbook': False,69 'songs/last directory import': '',
70 'songs/display written by': True,70 'songs/last directory export': '',
71 'songs/display copyright symbol': False,
72 'songs/last directory import': None,
73 'songs/last directory export': None,
74 'songs/songselect username': '',71 'songs/songselect username': '',
75 'songs/songselect password': '',72 'songs/songselect password': '',
76 'songs/songselect searches': '',73 'songs/songselect searches': '',
@@ -78,6 +75,59 @@
78 'songs/chord notation': 'english', # Can be english, german or neo-latin75 'songs/chord notation': 'english', # Can be english, german or neo-latin
79 'songs/mainview chords': False,76 'songs/mainview chords': False,
80 'songs/disable chords import': False,77 'songs/disable chords import': False,
78 'songs/footer template': """\
79${title}<br/>
80
81%if authors_none:
82 <%
83 authors = ", ".join(authors_none)
84 %>
85 ${authors_none_label}:&nbsp;${authors}<br/>
86%endif
87
88%if authors_words_music:
89 <%
90 authors = ", ".join(authors_words_music)
91 %>
92 ${authors_words_music_label}:&nbsp;${authors}<br/>
93%endif
94
95%if authors_words:
96 <%
97 authors = ", ".join(authors_words)
98 %>
99 ${authors_words_label}:&nbsp;${authors}<br/>
100%endif
101
102%if authors_music:
103 <%
104 authors = ", ".join(authors_music)
105 %>
106 ${authors_music_label}:&nbsp;${authors}<br/>
107%endif
108
109%if authors_translation:
110 <%
111 authors = ", ".join(authors_translation)
112 %>
113 ${authors_translation_label}:&nbsp;${authors}<br/>
114%endif
115
116%if copyright:
117 &copy;&nbsp;${copyright}<br/>
118%endif
119
120%if songbook_entries:
121 <%
122 entries = ", ".join(songbook_entries)
123 %>
124 ${entries}<br/>
125%endif
126
127%if ccli_license:
128 ${ccli_license_label}&nbsp;${ccli_license}<br/>
129%endif
130""",
81}131}
82132
83133
84134
=== modified file 'tests/functional/openlp_plugins/songs/test_mediaitem.py'
--- tests/functional/openlp_plugins/songs/test_mediaitem.py 2019-02-14 15:09:09 +0000
+++ tests/functional/openlp_plugins/songs/test_mediaitem.py 2019-02-21 21:27:51 +0000
@@ -34,6 +34,62 @@
34from openlp.plugins.songs.lib.mediaitem import SongMediaItem34from openlp.plugins.songs.lib.mediaitem import SongMediaItem
35from tests.helpers.testmixin import TestMixin35from tests.helpers.testmixin import TestMixin
3636
37__default_settings__ = {
38 'songs/footer template': """
39${title}<br/>
40
41%if authors_none:
42 <%
43 authors = ", ".join(authors_none)
44 %>
45 ${authors_none_label}:&nbsp;${authors}<br/>
46%endif
47
48%if authors_words_music:
49 <%
50 authors = ", ".join(authors_words_music)
51 %>
52 ${authors_words_music_label}:&nbsp;${authors}<br/>
53%endif
54
55%if authors_words:
56 <%
57 authors = ", ".join(authors_words)
58 %>
59 ${authors_words_label}:&nbsp;${authors}<br/>
60%endif
61
62%if authors_music:
63 <%
64 authors = ", ".join(authors_music)
65 %>
66 ${authors_music_label}:&nbsp;${authors}<br/>
67%endif
68
69%if authors_translation:
70 <%
71 authors = ", ".join(authors_translation)
72 %>
73 ${authors_translation_label}:&nbsp;${authors}<br/>
74%endif
75
76%if copyright:
77 &copy;&nbsp;${copyright}<br/>
78%endif
79
80%if songbook_entries:
81 <%
82 entries = ", ".join(songbook_entries)
83 %>
84 ${entries}<br/>
85%endif
86
87%if ccli_license:
88 ${ccli_license_label}&nbsp;${ccli_license}<br/>
89%endif
90"""
91}
92
3793
38class TestMediaItem(TestCase, TestMixin):94class TestMediaItem(TestCase, TestMixin):
39 """95 """
@@ -61,6 +117,7 @@
61 self.media_item.display_copyright_symbol = False117 self.media_item.display_copyright_symbol = False
62 self.setup_application()118 self.setup_application()
63 self.build_settings()119 self.build_settings()
120 Settings().extend_default_settings(__default_settings__)
64 QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))121 QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
65122
66 def tearDown(self):123 def tearDown(self):
@@ -297,63 +354,45 @@
297 """354 """
298 Test build songs footer with basic song and one author355 Test build songs footer with basic song and one author
299 """356 """
300 # GIVEN: A Song and a Service Item, mocked settings: True for 'songs/display written by'357 # GIVEN: A Song and a Service Item, mocked settings
301 # and False for 'core/ccli number' (ccli will cause traceback if true)358
302359 mocked_settings = MagicMock()
303 mocked_settings = MagicMock()360 mocked_settings.value.side_effect = [False, "", "0"]
304 mocked_settings.value.side_effect = [True, False]361 MockedSettings.return_value = mocked_settings
305 MockedSettings.return_value = mocked_settings362
306363 with patch('mako.template.Template.render_unicode') as MockedRenderer:
307 mock_song = MagicMock()364 mock_song = MagicMock()
308 mock_song.title = 'My Song'365 mock_song.title = 'My Song'
309 mock_song.authors_songs = []366 mock_song.alternate_title = ''
310 mock_author = MagicMock()367 mock_song.ccli_number = ''
311 mock_author.display_name = 'my author'368 mock_song.authors_songs = []
312 mock_author_song = MagicMock()369 mock_author = MagicMock()
313 mock_author_song.author = mock_author370 mock_author.display_name = 'my author'
314 mock_song.authors_songs.append(mock_author_song)371 mock_author_song = MagicMock()
315 mock_song.copyright = 'My copyright'372 mock_author_song.author = mock_author
316 service_item = ServiceItem(None)373 mock_song.authors_songs.append(mock_author_song)
317374 mock_song.copyright = 'My copyright'
318 # WHEN: I generate the Footer with default settings375 mock_song.songbook_entries = []
319 author_list = self.media_item.generate_footer(service_item, mock_song)376 service_item = ServiceItem(None)
320377
321 # THEN: I get the following Array returned378 # WHEN: I generate the Footer with default settings
322 assert service_item.raw_footer == ['My Song', 'Written by: my author', 'My copyright'], \379 author_list = self.media_item.generate_footer(service_item, mock_song)
323 'The array should be returned correctly with a song, one author and copyright'380
324 assert author_list == ['my author'], 'The author list should be returned correctly with one author'381 # THEN: The mako function was called with the following arguments
325382 args = {'authors_translation': [], 'authors_music_label': 'Music',
326 @patch(u'openlp.plugins.songs.lib.mediaitem.Settings')383 'copyright': 'My copyright', 'songbook_entries': [],
327 def test_build_song_footer_one_author_hide_written_by(self, MockedSettings):384 'alternate_title': '', 'topics': [], 'authors_music_all': [],
328 """385 'authors_words_label': 'Words', 'authors_music': [],
329 Test build songs footer with basic song and one author386 'authors_words_music': [], 'ccli_number': '',
330 """387 'authors_none_label': 'Written by', 'title': 'My Song',
331 # GIVEN: A Song and a Service Item, mocked settings: False for 'songs/display written by'388 'authors_words_music_label': 'Words and Music',
332 # and False for 'core/ccli number' (ccli will cause traceback if true)389 'authors_none': ['my author'],
333390 'ccli_license_label': 'CCLI License', 'authors_words': [],
334 mocked_settings = MagicMock()391 'ccli_license': '0', 'authors_translation_label': 'Translation',
335 mocked_settings.value.side_effect = [False, False]392 'authors_words_all': []}
336 MockedSettings.return_value = mocked_settings393 MockedRenderer.assert_called_once_with(**args)
337394 self.assertEqual(author_list, ['my author'],
338 mock_song = MagicMock()395 'The author list should be returned correctly with one author')
339 mock_song.title = 'My Song'
340 mock_song.authors_songs = []
341 mock_author = MagicMock()
342 mock_author.display_name = 'my author'
343 mock_author_song = MagicMock()
344 mock_author_song.author = mock_author
345 mock_song.authors_songs.append(mock_author_song)
346 mock_song.copyright = 'My copyright'
347 service_item = ServiceItem(None)
348
349 # WHEN: I generate the Footer with default settings
350 author_list = self.media_item.generate_footer(service_item, mock_song)
351
352 # THEN: I get the following Array returned
353 assert service_item.raw_footer == ['My Song', 'my author', 'My copyright'], \
354 'The array should be returned correctly with a song, one author and copyright, ' \
355 'text Written by should not be part of the text.'
356 assert author_list == ['my author'], 'The author list should be returned correctly with one author'
357396
358 def test_build_song_footer_two_authors(self):397 def test_build_song_footer_two_authors(self):
359 """398 """
@@ -382,6 +421,7 @@
382 mock_author_song.author_type = AuthorType.Translation421 mock_author_song.author_type = AuthorType.Translation
383 mock_song.authors_songs.append(mock_author_song)422 mock_song.authors_songs.append(mock_author_song)
384 mock_song.copyright = 'My copyright'423 mock_song.copyright = 'My copyright'
424 mock_song.songbook_entries = []
385 service_item = ServiceItem(None)425 service_item = ServiceItem(None)
386426
387 # WHEN: I generate the Footer with default settings427 # WHEN: I generate the Footer with default settings
@@ -389,7 +429,7 @@
389429
390 # THEN: I get the following Array returned430 # THEN: I get the following Array returned
391 assert service_item.raw_footer == ['My Song', 'Words: another author', 'Music: my author',431 assert service_item.raw_footer == ['My Song', 'Words: another author', 'Music: my author',
392 'Translation: translator', 'My copyright'], \432 'Translation: translator', '© My copyright'], \
393 'The array should be returned correctly with a song, two authors and copyright'433 'The array should be returned correctly with a song, two authors and copyright'
394 assert author_list == ['another author', 'my author', 'translator'], \434 assert author_list == ['another author', 'my author', 'translator'], \
395 'The author list should be returned correctly with two authors'435 'The author list should be returned correctly with two authors'
@@ -402,6 +442,7 @@
402 mock_song = MagicMock()442 mock_song = MagicMock()
403 mock_song.title = 'My Song'443 mock_song.title = 'My Song'
404 mock_song.copyright = 'My copyright'444 mock_song.copyright = 'My copyright'
445 mock_song.songbook_entries = []
405 service_item = ServiceItem(None)446 service_item = ServiceItem(None)
406 Settings().setValue('core/ccli number', '1234')447 Settings().setValue('core/ccli number', '1234')
407448
@@ -409,7 +450,7 @@
409 self.media_item.generate_footer(service_item, mock_song)450 self.media_item.generate_footer(service_item, mock_song)
410451
411 # THEN: I get the following Array returned452 # THEN: I get the following Array returned
412 assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 1234'], \453 assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 1234'], \
413 'The array should be returned correctly with a song, an author, copyright and ccli'454 'The array should be returned correctly with a song, an author, copyright and ccli'
414455
415 # WHEN: I amend the CCLI value456 # WHEN: I amend the CCLI value
@@ -417,7 +458,7 @@
417 self.media_item.generate_footer(service_item, mock_song)458 self.media_item.generate_footer(service_item, mock_song)
418459
419 # THEN: I would get an amended footer string460 # THEN: I would get an amended footer string
420 assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 4321'], \461 assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 4321'], \
421 'The array should be returned correctly with a song, an author, copyright and amended ccli'462 'The array should be returned correctly with a song, an author, copyright and amended ccli'
422463
423 def test_build_song_footer_base_songbook(self):464 def test_build_song_footer_base_songbook(self):
@@ -431,6 +472,8 @@
431 song.copyright = 'My copyright'472 song.copyright = 'My copyright'
432 song.authors_songs = []473 song.authors_songs = []
433 song.songbook_entries = []474 song.songbook_entries = []
475 song.alternate_title = ''
476 song.topics = []
434 song.ccli_number = ''477 song.ccli_number = ''
435 book1 = MagicMock()478 book1 = MagicMock()
436 book1.name = 'My songbook'479 book1.name = 'My songbook'
@@ -444,15 +487,8 @@
444 # WHEN: I generate the Footer with default settings487 # WHEN: I generate the Footer with default settings
445 self.media_item.generate_footer(service_item, song)488 self.media_item.generate_footer(service_item, song)
446489
447 # THEN: The songbook should not be in the footer
448 assert service_item.raw_footer == ['My Song', 'My copyright']
449
450 # WHEN: I activate the "display songbook" option
451 self.media_item.display_songbook = True
452 self.media_item.generate_footer(service_item, song)
453
454 # THEN: The songbook should be in the footer490 # THEN: The songbook should be in the footer
455 assert service_item.raw_footer == ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']491 assert service_item.raw_footer == ['My Song', '© My copyright', 'My songbook #12, Thy songbook #502A']
456492
457 def test_build_song_footer_copyright_enabled(self):493 def test_build_song_footer_copyright_enabled(self):
458 """494 """
@@ -463,6 +499,7 @@
463 mock_song = MagicMock()499 mock_song = MagicMock()
464 mock_song.title = 'My Song'500 mock_song.title = 'My Song'
465 mock_song.copyright = 'My copyright'501 mock_song.copyright = 'My copyright'
502 mock_song.songbook_entries = []
466 service_item = ServiceItem(None)503 service_item = ServiceItem(None)
467504
468 # WHEN: I generate the Footer with default settings505 # WHEN: I generate the Footer with default settings
@@ -479,13 +516,14 @@
479 mock_song = MagicMock()516 mock_song = MagicMock()
480 mock_song.title = 'My Song'517 mock_song.title = 'My Song'
481 mock_song.copyright = 'My copyright'518 mock_song.copyright = 'My copyright'
519 mock_song.songbook_entries = []
482 service_item = ServiceItem(None)520 service_item = ServiceItem(None)
483521
484 # WHEN: I generate the Footer with default settings522 # WHEN: I generate the Footer with default settings
485 self.media_item.generate_footer(service_item, mock_song)523 self.media_item.generate_footer(service_item, mock_song)
486524
487 # THEN: The copyright symbol should not be in the footer525 # THEN: The copyright symbol should not be in the footer
488 assert service_item.raw_footer == ['My Song', 'My copyright']526 assert service_item.raw_footer == ['My Song', '© My copyright']
489527
490 def test_authors_match(self):528 def test_authors_match(self):
491 """529 """