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: 735 lines (+285/-161)
7 files modified
openlp/core/lib/serviceitem.py (+4/-7)
openlp/core/ui/maindisplay.py (+2/-2)
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
Tim Bentley Pending
Raoul Snyman Pending
Review via email: mp+325552@code.launchpad.net

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

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

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)

lp:~sam92/openlp/bug-1695620 (revision 2759)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2086/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1996/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1910/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1288/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1137/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/266/
[SUCCESS] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/111/

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
lp:~sam92/openlp/bug-1695620 updated
2760. By Samuel Mehrbrodt

Merge trunk

Revision history for this message
Samuel Mehrbrodt (sam92) wrote :

So is there a chance this can be reviewed now?

> Blank lines!
What do you mean with that?

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

Remove blank lines

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
1=== modified file 'openlp/core/lib/serviceitem.py'
2--- openlp/core/lib/serviceitem.py 2018-11-18 17:29:47 +0000
3+++ openlp/core/lib/serviceitem.py 2019-02-08 13:39:53 +0000
4@@ -173,7 +173,8 @@
5 self.items = []
6 self.icon = UiIcons().default
7 self.raw_footer = []
8- self.foot_text = ''
9+ # Plugins can set footer_html themselves. If they don't, it will be generated from raw_footer.
10+ self.footer_html = ''
11 self.theme = None
12 self.service_item_type = None
13 self._raw_frames = []
14@@ -295,12 +296,8 @@
15 else:
16 log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
17 self.title = clean_tags(self.title)
18- # The footer should never be None, but to be compatible with a few
19- # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
20- # avoid tracebacks.
21- if self.raw_footer is None:
22- self.raw_footer = []
23- self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
24+ if not self.footer_html:
25+ self.footer_html = '<br>'.join([_f for _f in self.raw_footer if _f])
26
27 def add_from_image(self, path, title, background=None, thumbnail=None):
28 """
29
30=== modified file 'openlp/core/ui/maindisplay.py'
31--- openlp/core/ui/maindisplay.py 2019-01-04 20:11:12 +0000
32+++ openlp/core/ui/maindisplay.py 2019-02-08 13:39:53 +0000
33@@ -475,8 +475,8 @@
34 created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes,
35 plugins=self.plugin_manager.plugins)
36 self.web_view.setHtml(created_html)
37- if service_item.foot_text:
38- self.footer(service_item.foot_text)
39+ if service_item.footer_html:
40+ self.footer(service_item.footer_html)
41 # if was hidden keep it hidden
42 if self.hide_mode and self.is_live and not service_item.is_media():
43 if Settings().value('core/auto unblank'):
44
45=== modified file 'openlp/core/ui/printserviceform.py'
46--- openlp/core/ui/printserviceform.py 2018-04-21 05:47:20 +0000
47+++ openlp/core/ui/printserviceform.py 2019-02-08 13:39:53 +0000
48@@ -234,11 +234,11 @@
49 for slide in range(len(item.get_frames())):
50 self._add_element('li', item.get_frame_title(slide), ol)
51 # add footer
52- foot_text = item.foot_text
53- foot_text = foot_text.partition('<br>')[2]
54- if foot_text:
55- foot_text = html.escape(foot_text.replace('<br>', '\n'))
56- self._add_element('div', foot_text.replace('\n', '<br>'), parent=div, class_id='itemFooter')
57+ footer_html = item.footer_html
58+ footer_html = footer_html.partition('<br>')[2]
59+ if footer_html:
60+ footer_html = html.escape(footer_html.replace('<br>', '\n'))
61+ self._add_element('div', footer_html.replace('\n', '<br>'), parent=div, classId='itemFooter')
62 # Add service items' notes.
63 if self.notes_check_box.isChecked():
64 if item.notes:
65
66=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
67--- openlp/plugins/songs/lib/mediaitem.py 2019-01-10 22:01:07 +0000
68+++ openlp/plugins/songs/lib/mediaitem.py 2019-02-08 13:39:53 +0000
69@@ -21,6 +21,7 @@
70 ###############################################################################
71 import logging
72 import os
73+import mako
74
75 from PyQt5 import QtCore, QtWidgets
76 from sqlalchemy.sql import and_, or_
77@@ -36,7 +37,7 @@
78 from openlp.core.lib.mediamanageritem import MediaManagerItem
79 from openlp.core.lib.plugin import PluginStatus
80 from openlp.core.lib.serviceitem import ItemCapabilities
81-from openlp.core.lib.ui import create_widget_action
82+from openlp.core.lib.ui import create_widget_action, critical_error_message_box
83 from openlp.plugins.songs.forms.editsongform import EditSongForm
84 from openlp.plugins.songs.forms.songexportform import SongExportForm
85 from openlp.plugins.songs.forms.songimportform import SongImportForm
86@@ -130,9 +131,6 @@
87 self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
88 self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
89 self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
90- self.display_songbook = Settings().value(self.settings_section + '/display songbook')
91- self.display_written_by_text = Settings().value(self.settings_section + '/display written by')
92- self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol')
93
94 def retranslateUi(self):
95 self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
96@@ -676,12 +674,8 @@
97 item.raw_footer = []
98 item.raw_footer.append(song.title)
99 if authors_none:
100- # If the setting for showing "Written by:" is enabled, show it before unspecified authors.
101- if Settings().value('songs/display written by'):
102- item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
103- authors=create_separated_list(authors_none)))
104- else:
105- item.raw_footer.append("{authors}".format(authors=create_separated_list(authors_none)))
106+ item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
107+ authors=create_separated_list(authors_none)))
108 if authors_words_music:
109 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
110 authors=create_separated_list(authors_words_music)))
111@@ -695,34 +689,44 @@
112 item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
113 authors=create_separated_list(authors_translation)))
114 if song.copyright:
115- if self.display_copyright_symbol:
116- item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
117- song=song.copyright))
118- else:
119- item.raw_footer.append(song.copyright)
120- if self.display_songbook and song.songbook_entries:
121- songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
122+ item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
123+ song=song.copyright))
124+ songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
125+ if song.songbook_entries:
126 item.raw_footer.append(", ".join(songbooks))
127 if Settings().value('core/ccli number'):
128- item.raw_footer.append(translate('SongsPlugin.MediaItem',
129- 'CCLI License: ') + Settings().value('core/ccli number'))
130- item.metadata.append('<em>{label}:</em> {title}'.format(label=translate('SongsPlugin.MediaItem', 'Title'),
131- title=song.title))
132- if song.alternate_title:
133- item.metadata.append('<em>{label}:</em> {title}'.
134- format(label=translate('SongsPlugin.MediaItem', 'Alt Title'),
135- title=song.alternate_title))
136- if song.songbook_entries:
137- for songbook_entry in song.songbook_entries:
138- item.metadata.append('<em>{label}:</em> {book}/{num}/{pub}'.
139- format(label=translate('SongsPlugin.MediaItem', 'Songbook'),
140- book=songbook_entry.songbook.name,
141- num=songbook_entry.entry,
142- pub=songbook_entry.songbook.publisher))
143- if song.topics:
144- for topics in song.topics:
145- item.metadata.append('<em>{label}:</em> {topic}'.
146- format(label=translate('SongsPlugin.MediaItem', 'Topic'), topic=topics.name))
147+ item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') +
148+ Settings().value('core/ccli number'))
149+ footer_template = Settings().value('songs/footer template')
150+ # Keep this in sync with the list in songstab.py
151+ vars = {
152+ 'title': song.title,
153+ 'alternate_title': song.alternate_title,
154+ 'authors_none_label': translate('OpenLP.Ui', 'Written by'),
155+ 'authors_none': authors_none,
156+ 'authors_words_label': AuthorType.Types[AuthorType.Words],
157+ 'authors_words': authors_words,
158+ 'authors_music_label': AuthorType.Types[AuthorType.Music],
159+ 'authors_music': authors_music,
160+ 'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic],
161+ 'authors_words_music': authors_words_music,
162+ 'authors_translation_label': AuthorType.Types[AuthorType.Translation],
163+ 'authors_translation': authors_translation,
164+ 'authors_words_all': authors_words + authors_words_music,
165+ 'authors_music_all': authors_music + authors_words_music,
166+ 'copyright': song.copyright,
167+ 'songbook_entries': songbooks,
168+ 'ccli_license': Settings().value('core/ccli number'),
169+ 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
170+ 'ccli_number': song.ccli_number,
171+ 'topics': [topic.name for topic in song.topics]
172+ }
173+ try:
174+ item.footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '')
175+ except mako.exceptions.SyntaxException:
176+ log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render())
177+ critical_error_message_box(message=translate('SongsPlugin.MediaItem',
178+ 'Failed to render Song footer html.\nSee log for details'))
179 return authors_all
180
181 def service_load(self, item):
182
183=== modified file 'openlp/plugins/songs/lib/songstab.py'
184--- openlp/plugins/songs/lib/songstab.py 2018-08-25 14:08:19 +0000
185+++ openlp/plugins/songs/lib/songstab.py 2019-02-08 13:39:53 +0000
186@@ -25,7 +25,7 @@
187 from openlp.core.common.i18n import translate
188 from openlp.core.common.settings import Settings
189 from openlp.core.lib.settingstab import SettingsTab
190-from openlp.plugins.songs.lib.ui import SongStrings
191+from openlp.plugins.songs.lib.db import AuthorType
192
193
194 class SongsTab(SettingsTab):
195@@ -54,15 +54,6 @@
196 self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)
197 self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')
198 self.mode_layout.addWidget(self.songbook_slide_check_box)
199- self.display_songbook_check_box = QtWidgets.QCheckBox(self.mode_group_box)
200- self.display_songbook_check_box.setObjectName('songbook_check_box')
201- self.mode_layout.addWidget(self.display_songbook_check_box)
202- self.display_written_by_check_box = QtWidgets.QCheckBox(self.mode_group_box)
203- self.display_written_by_check_box.setObjectName('written_by_check_box')
204- self.mode_layout.addWidget(self.display_written_by_check_box)
205- self.display_copyright_check_box = QtWidgets.QCheckBox(self.mode_group_box)
206- self.display_copyright_check_box.setObjectName('copyright_check_box')
207- self.mode_layout.addWidget(self.display_copyright_check_box)
208 self.left_layout.addWidget(self.mode_group_box)
209 # Chords group box
210 self.chords_group_box = QtWidgets.QGroupBox(self.left_column)
211@@ -93,20 +84,34 @@
212 self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button')
213 self.chords_layout.addWidget(self.neolatin_notation_radio_button)
214 self.left_layout.addWidget(self.chords_group_box)
215+ # Footer group box
216+ self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
217+ self.footer_group_box.setObjectName('footer_group_box')
218+ self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box)
219+ self.footer_layout.setObjectName('chords_layout')
220+ self.footer_info_label = QtWidgets.QLabel(self.footer_group_box)
221+ self.footer_layout.addWidget(self.footer_info_label)
222+ self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box)
223+ self.footer_layout.addWidget(self.footer_placeholder_info)
224+ self.footer_desc_label = QtWidgets.QLabel(self.footer_group_box)
225+ self.footer_layout.addWidget(self.footer_desc_label)
226+ self.footer_edit_box = QtWidgets.QTextEdit(self.footer_group_box)
227+ self.footer_layout.addWidget(self.footer_edit_box)
228+ self.footer_reset_button = QtWidgets.QPushButton(self.footer_group_box)
229+ self.footer_layout.addWidget(self.footer_reset_button, alignment=QtCore.Qt.AlignRight)
230+ self.right_layout.addWidget(self.footer_group_box)
231 self.left_layout.addStretch()
232 self.right_layout.addStretch()
233 self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
234 self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
235 self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
236 self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed)
237- self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
238- self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
239- self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
240 self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed)
241 self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed)
242 self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked)
243 self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked)
244 self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked)
245+ self.footer_reset_button.clicked.connect(self.on_footer_reset_button_clicked)
246
247 def retranslateUi(self):
248 self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
249@@ -117,12 +122,6 @@
250 'Import missing songs from Service files'))
251 self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab',
252 'Add Songbooks as first side'))
253- self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
254- self.display_written_by_check_box.setText(translate(
255- 'SongsPlugin.SongsTab', 'Show "Written by:" in footer for unspecified authors'))
256- self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
257- 'Display "{symbol}" symbol before copyright '
258- 'info').format(symbol=SongStrings.CopyrightSymbol))
259 self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will '
260 'be regarded as chords.'))
261 self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
262@@ -134,6 +133,53 @@
263 self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
264 self.neolatin_notation_radio_button.setText(
265 translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)')
266+ self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer'))
267+ # Keep this in sync with the list in mediaitem.py
268+ const = '<code>"{}"</code>'
269+ placeholders = [
270+ # placeholder, description, can be empty, is a list
271+ ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False],
272+ ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), True, False],
273+ ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False],
274+ ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), False, True],
275+ ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False],
276+ ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type "Words")'), False, True],
277+ ['authors_music_label', const.format(AuthorType.Types[AuthorType.Music]), False, False],
278+ ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Music")'), False, True],
279+ ['authors_words_music_label', const.format(AuthorType.Types[AuthorType.WordsAndMusic]), False, False],
280+ ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Words and Music")'), False, True],
281+ ['authors_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False],
282+ ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type "Translation")'), False, True],
283+ ['authors_words_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Words" & "Words and Music")'),
284+ False, True],
285+ ['authors_music_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Music" & "Words and Music")'),
286+ False, True],
287+ ['copyright', translate('SongsPlugin.SongsTab', 'Copyright information'), True, False],
288+ ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), False, True],
289+ ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False],
290+ ['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False],
291+ ['ccli_number', translate('SongsPlugin.SongsTab', 'Song CCLI Number'), True, False],
292+ ['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True],
293+ ]
294+ placeholder_info = '<table style="background: #eee">\n<tr><th><b>{ph}</b></th><th><b>{desc}</b></th></tr>\n'\
295+ .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'),
296+ desc=translate('SongsPlugin.SongsTab', 'Description'))
297+ for placeholder in placeholders:
298+ placeholder_info += '<tr><td>${{{pl}}}</td><td>{des}{opt}</td></tr>\n'\
299+ .format(pl=placeholder[0], des=placeholder[1],
300+ opt=('&nbsp;¹' if placeholder[2] else '') +
301+ ('&nbsp;²' if placeholder[3] else ''))
302+ placeholder_info += '</table>'
303+ placeholder_info += '\n<br/>¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty'))
304+ placeholder_info += '\n<br/>² {}'.format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty'))
305+ self.footer_placeholder_info.setHtml(placeholder_info)
306+ self.footer_placeholder_info.setReadOnly(True)
307+
308+ self.footer_info_label.setText(translate('SongsPlugin.SongsTab', 'How to use Footers:'))
309+ self.footer_desc_label.setText('{} (<a href="http://docs.makotemplates.org">{}</a>):'
310+ .format(translate('SongsPlugin.SongsTab', 'Footer Template'),
311+ translate('SongsPlugin.SongsTab', 'Mako Syntax')))
312+ self.footer_reset_button.setText(translate('SongsPlugin.SongsTab', 'Reset Template'))
313
314 def on_search_as_type_check_box_changed(self, check_state):
315 self.song_search = (check_state == QtCore.Qt.Checked)
316@@ -150,15 +196,6 @@
317 def on_songbook_slide_check_box_changed(self, check_state):
318 self.songbook_slide = (check_state == QtCore.Qt.Checked)
319
320- def on_songbook_check_box_changed(self, check_state):
321- self.display_songbook = (check_state == QtCore.Qt.Checked)
322-
323- def on_written_by_check_box_changed(self, check_state):
324- self.display_written_by = (check_state == QtCore.Qt.Checked)
325-
326- def on_copyright_check_box_changed(self, check_state):
327- self.display_copyright_symbol = (check_state == QtCore.Qt.Checked)
328-
329 def on_mainview_chords_check_box_changed(self, check_state):
330 self.mainview_chords = (check_state == QtCore.Qt.Checked)
331
332@@ -174,6 +211,9 @@
333 def on_neolatin_notation_button_clicked(self):
334 self.chord_notation = 'neo-latin'
335
336+ def on_footer_reset_button_clicked(self):
337+ self.footer_edit_box.setPlainText(Settings().get_default_value('songs/footer template'))
338+
339 def load(self):
340 settings = Settings()
341 settings.beginGroup(self.settings_section)
342@@ -181,9 +221,6 @@
343 self.update_edit = settings.value('update service on edit')
344 self.update_load = settings.value('add song from service')
345 self.songbook_slide = settings.value('add songbook slide')
346- self.display_songbook = settings.value('display songbook')
347- self.display_written_by = settings.value('display written by')
348- self.display_copyright_symbol = settings.value('display copyright symbol')
349 self.enable_chords = settings.value('enable chords')
350 self.chord_notation = settings.value('chord notation')
351 self.mainview_chords = settings.value('mainview chords')
352@@ -191,9 +228,6 @@
353 self.tool_bar_active_check_box.setChecked(self.tool_bar)
354 self.update_on_edit_check_box.setChecked(self.update_edit)
355 self.add_from_service_check_box.setChecked(self.update_load)
356- self.display_songbook_check_box.setChecked(self.display_songbook)
357- self.display_written_by_check_box.setChecked(self.display_written_by)
358- self.display_copyright_check_box.setChecked(self.display_copyright_symbol)
359 self.chords_group_box.setChecked(self.enable_chords)
360 self.mainview_chords_check_box.setChecked(self.mainview_chords)
361 self.disable_chords_import_check_box.setChecked(self.disable_chords_import)
362@@ -203,6 +237,7 @@
363 self.neolatin_notation_radio_button.setChecked(True)
364 else:
365 self.english_notation_radio_button.setChecked(True)
366+ self.footer_edit_box.setPlainText(settings.value('footer template'))
367 settings.endGroup()
368
369 def save(self):
370@@ -211,13 +246,13 @@
371 settings.setValue('display songbar', self.tool_bar)
372 settings.setValue('update service on edit', self.update_edit)
373 settings.setValue('add song from service', self.update_load)
374- settings.setValue('display songbook', self.display_songbook)
375- settings.setValue('display written by', self.display_written_by)
376- settings.setValue('display copyright symbol', self.display_copyright_symbol)
377 settings.setValue('enable chords', self.chords_group_box.isChecked())
378 settings.setValue('mainview chords', self.mainview_chords)
379 settings.setValue('disable chords import', self.disable_chords_import)
380 settings.setValue('chord notation', self.chord_notation)
381+ # Only save footer template if it has been changed. This allows future updates
382+ if self.footer_edit_box.toPlainText() != Settings().get_default_value('songs/footer template'):
383+ settings.setValue('footer template', self.footer_edit_box.toPlainText())
384 settings.setValue('add songbook slide', self.songbook_slide)
385 settings.endGroup()
386 if self.tab_visited:
387
388=== modified file 'openlp/plugins/songs/songsplugin.py'
389--- openlp/plugins/songs/songsplugin.py 2018-10-26 18:30:59 +0000
390+++ openlp/plugins/songs/songsplugin.py 2019-02-08 13:39:53 +0000
391@@ -66,11 +66,8 @@
392 'songs/add song from service': True,
393 'songs/add songbook slide': False,
394 'songs/display songbar': True,
395- 'songs/display songbook': False,
396- 'songs/display written by': True,
397- 'songs/display copyright symbol': False,
398- 'songs/last directory import': None,
399- 'songs/last directory export': None,
400+ 'songs/last directory import': '',
401+ 'songs/last directory export': '',
402 'songs/songselect username': '',
403 'songs/songselect password': '',
404 'songs/songselect searches': '',
405@@ -78,6 +75,59 @@
406 'songs/chord notation': 'english', # Can be english, german or neo-latin
407 'songs/mainview chords': False,
408 'songs/disable chords import': False,
409+ 'songs/footer template': """\
410+${title}<br/>
411+
412+%if authors_none:
413+ <%
414+ authors = ", ".join(authors_none)
415+ %>
416+ ${authors_none_label}:&nbsp;${authors}<br/>
417+%endif
418+
419+%if authors_words_music:
420+ <%
421+ authors = ", ".join(authors_words_music)
422+ %>
423+ ${authors_words_music_label}:&nbsp;${authors}<br/>
424+%endif
425+
426+%if authors_words:
427+ <%
428+ authors = ", ".join(authors_words)
429+ %>
430+ ${authors_words_label}:&nbsp;${authors}<br/>
431+%endif
432+
433+%if authors_music:
434+ <%
435+ authors = ", ".join(authors_music)
436+ %>
437+ ${authors_music_label}:&nbsp;${authors}<br/>
438+%endif
439+
440+%if authors_translation:
441+ <%
442+ authors = ", ".join(authors_translation)
443+ %>
444+ ${authors_translation_label}:&nbsp;${authors}<br/>
445+%endif
446+
447+%if copyright:
448+ &copy;&nbsp;${copyright}<br/>
449+%endif
450+
451+%if songbook_entries:
452+ <%
453+ entries = ", ".join(songbook_entries)
454+ %>
455+ ${entries}<br/>
456+%endif
457+
458+%if ccli_license:
459+ ${ccli_license_label}&nbsp;${ccli_license}<br/>
460+%endif
461+""",
462 }
463
464
465
466=== modified file 'tests/functional/openlp_plugins/songs/test_mediaitem.py'
467--- tests/functional/openlp_plugins/songs/test_mediaitem.py 2019-01-04 20:11:12 +0000
468+++ tests/functional/openlp_plugins/songs/test_mediaitem.py 2019-02-08 13:39:53 +0000
469@@ -34,6 +34,62 @@
470 from openlp.plugins.songs.lib.mediaitem import SongMediaItem
471 from tests.helpers.testmixin import TestMixin
472
473+__default_settings__ = {
474+ 'songs/footer template': """
475+${title}<br/>
476+
477+%if authors_none:
478+ <%
479+ authors = ", ".join(authors_none)
480+ %>
481+ ${authors_none_label}:&nbsp;${authors}<br/>
482+%endif
483+
484+%if authors_words_music:
485+ <%
486+ authors = ", ".join(authors_words_music)
487+ %>
488+ ${authors_words_music_label}:&nbsp;${authors}<br/>
489+%endif
490+
491+%if authors_words:
492+ <%
493+ authors = ", ".join(authors_words)
494+ %>
495+ ${authors_words_label}:&nbsp;${authors}<br/>
496+%endif
497+
498+%if authors_music:
499+ <%
500+ authors = ", ".join(authors_music)
501+ %>
502+ ${authors_music_label}:&nbsp;${authors}<br/>
503+%endif
504+
505+%if authors_translation:
506+ <%
507+ authors = ", ".join(authors_translation)
508+ %>
509+ ${authors_translation_label}:&nbsp;${authors}<br/>
510+%endif
511+
512+%if copyright:
513+ &copy;&nbsp;${copyright}<br/>
514+%endif
515+
516+%if songbook_entries:
517+ <%
518+ entries = ", ".join(songbook_entries)
519+ %>
520+ ${entries}<br/>
521+%endif
522+
523+%if ccli_license:
524+ ${ccli_license_label}&nbsp;${ccli_license}<br/>
525+%endif
526+"""
527+}
528+
529
530 class TestMediaItem(TestCase, TestMixin):
531 """
532@@ -61,6 +117,7 @@
533 self.media_item.display_copyright_symbol = False
534 self.setup_application()
535 self.build_settings()
536+ Settings().extend_default_settings(__default_settings__)
537 QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
538
539 def tearDown(self):
540@@ -297,63 +354,45 @@
541 """
542 Test build songs footer with basic song and one author
543 """
544- # GIVEN: A Song and a Service Item, mocked settings: True for 'songs/display written by'
545- # and False for 'core/ccli number' (ccli will cause traceback if true)
546-
547- mocked_settings = MagicMock()
548- mocked_settings.value.side_effect = [True, False]
549- MockedSettings.return_value = mocked_settings
550-
551- mock_song = MagicMock()
552- mock_song.title = 'My Song'
553- mock_song.authors_songs = []
554- mock_author = MagicMock()
555- mock_author.display_name = 'my author'
556- mock_author_song = MagicMock()
557- mock_author_song.author = mock_author
558- mock_song.authors_songs.append(mock_author_song)
559- mock_song.copyright = 'My copyright'
560- service_item = ServiceItem(None)
561-
562- # WHEN: I generate the Footer with default settings
563- author_list = self.media_item.generate_footer(service_item, mock_song)
564-
565- # THEN: I get the following Array returned
566- assert service_item.raw_footer == ['My Song', 'Written by: my author', 'My copyright'], \
567- 'The array should be returned correctly with a song, one author and copyright'
568- assert author_list == ['my author'], 'The author list should be returned correctly with one author'
569-
570- @patch(u'openlp.plugins.songs.lib.mediaitem.Settings')
571- def test_build_song_footer_one_author_hide_written_by(self, MockedSettings):
572- """
573- Test build songs footer with basic song and one author
574- """
575- # GIVEN: A Song and a Service Item, mocked settings: False for 'songs/display written by'
576- # and False for 'core/ccli number' (ccli will cause traceback if true)
577-
578- mocked_settings = MagicMock()
579- mocked_settings.value.side_effect = [False, False]
580- MockedSettings.return_value = mocked_settings
581-
582- mock_song = MagicMock()
583- mock_song.title = 'My Song'
584- mock_song.authors_songs = []
585- mock_author = MagicMock()
586- mock_author.display_name = 'my author'
587- mock_author_song = MagicMock()
588- mock_author_song.author = mock_author
589- mock_song.authors_songs.append(mock_author_song)
590- mock_song.copyright = 'My copyright'
591- service_item = ServiceItem(None)
592-
593- # WHEN: I generate the Footer with default settings
594- author_list = self.media_item.generate_footer(service_item, mock_song)
595-
596- # THEN: I get the following Array returned
597- assert service_item.raw_footer == ['My Song', 'my author', 'My copyright'], \
598- 'The array should be returned correctly with a song, one author and copyright, ' \
599- 'text Written by should not be part of the text.'
600- assert author_list == ['my author'], 'The author list should be returned correctly with one author'
601+ # GIVEN: A Song and a Service Item, mocked settings
602+
603+ mocked_settings = MagicMock()
604+ mocked_settings.value.side_effect = [False, "", "0"]
605+ MockedSettings.return_value = mocked_settings
606+
607+ with patch('mako.template.Template.render_unicode') as MockedRenderer:
608+ mock_song = MagicMock()
609+ mock_song.title = 'My Song'
610+ mock_song.alternate_title = ''
611+ mock_song.ccli_number = ''
612+ mock_song.authors_songs = []
613+ mock_author = MagicMock()
614+ mock_author.display_name = 'my author'
615+ mock_author_song = MagicMock()
616+ mock_author_song.author = mock_author
617+ mock_song.authors_songs.append(mock_author_song)
618+ mock_song.copyright = 'My copyright'
619+ mock_song.songbook_entries = []
620+ service_item = ServiceItem(None)
621+
622+ # WHEN: I generate the Footer with default settings
623+ author_list = self.media_item.generate_footer(service_item, mock_song)
624+
625+ # THEN: The mako function was called with the following arguments
626+ args = {'authors_translation': [], 'authors_music_label': 'Music',
627+ 'copyright': 'My copyright', 'songbook_entries': [],
628+ 'alternate_title': '', 'topics': [], 'authors_music_all': [],
629+ 'authors_words_label': 'Words', 'authors_music': [],
630+ 'authors_words_music': [], 'ccli_number': '',
631+ 'authors_none_label': 'Written by', 'title': 'My Song',
632+ 'authors_words_music_label': 'Words and Music',
633+ 'authors_none': ['my author'],
634+ 'ccli_license_label': 'CCLI License', 'authors_words': [],
635+ 'ccli_license': '0', 'authors_translation_label': 'Translation',
636+ 'authors_words_all': []}
637+ MockedRenderer.assert_called_once_with(**args)
638+ self.assertEqual(author_list, ['my author'],
639+ 'The author list should be returned correctly with one author')
640
641 def test_build_song_footer_two_authors(self):
642 """
643@@ -382,6 +421,7 @@
644 mock_author_song.author_type = AuthorType.Translation
645 mock_song.authors_songs.append(mock_author_song)
646 mock_song.copyright = 'My copyright'
647+ mock_song.songbook_entries = []
648 service_item = ServiceItem(None)
649
650 # WHEN: I generate the Footer with default settings
651@@ -389,7 +429,7 @@
652
653 # THEN: I get the following Array returned
654 assert service_item.raw_footer == ['My Song', 'Words: another author', 'Music: my author',
655- 'Translation: translator', 'My copyright'], \
656+ 'Translation: translator', '© My copyright'], \
657 'The array should be returned correctly with a song, two authors and copyright'
658 assert author_list == ['another author', 'my author', 'translator'], \
659 'The author list should be returned correctly with two authors'
660@@ -402,6 +442,7 @@
661 mock_song = MagicMock()
662 mock_song.title = 'My Song'
663 mock_song.copyright = 'My copyright'
664+ mock_song.songbook_entries = []
665 service_item = ServiceItem(None)
666 Settings().setValue('core/ccli number', '1234')
667
668@@ -409,7 +450,7 @@
669 self.media_item.generate_footer(service_item, mock_song)
670
671 # THEN: I get the following Array returned
672- assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 1234'], \
673+ assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 1234'], \
674 'The array should be returned correctly with a song, an author, copyright and ccli'
675
676 # WHEN: I amend the CCLI value
677@@ -417,7 +458,7 @@
678 self.media_item.generate_footer(service_item, mock_song)
679
680 # THEN: I would get an amended footer string
681- assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 4321'], \
682+ assert service_item.raw_footer == ['My Song', '© My copyright', 'CCLI License: 4321'], \
683 'The array should be returned correctly with a song, an author, copyright and amended ccli'
684
685 def test_build_song_footer_base_songbook(self):
686@@ -431,6 +472,8 @@
687 song.copyright = 'My copyright'
688 song.authors_songs = []
689 song.songbook_entries = []
690+ song.alternate_title = ''
691+ song.topics = []
692 song.ccli_number = ''
693 book1 = MagicMock()
694 book1.name = 'My songbook'
695@@ -444,15 +487,8 @@
696 # WHEN: I generate the Footer with default settings
697 self.media_item.generate_footer(service_item, song)
698
699- # THEN: The songbook should not be in the footer
700- assert service_item.raw_footer == ['My Song', 'My copyright']
701-
702- # WHEN: I activate the "display songbook" option
703- self.media_item.display_songbook = True
704- self.media_item.generate_footer(service_item, song)
705-
706 # THEN: The songbook should be in the footer
707- assert service_item.raw_footer == ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']
708+ assert service_item.raw_footer == ['My Song', '© My copyright', 'My songbook #12, Thy songbook #502A']
709
710 def test_build_song_footer_copyright_enabled(self):
711 """
712@@ -463,6 +499,7 @@
713 mock_song = MagicMock()
714 mock_song.title = 'My Song'
715 mock_song.copyright = 'My copyright'
716+ mock_song.songbook_entries = []
717 service_item = ServiceItem(None)
718
719 # WHEN: I generate the Footer with default settings
720@@ -479,13 +516,14 @@
721 mock_song = MagicMock()
722 mock_song.title = 'My Song'
723 mock_song.copyright = 'My copyright'
724+ mock_song.songbook_entries = []
725 service_item = ServiceItem(None)
726
727 # WHEN: I generate the Footer with default settings
728 self.media_item.generate_footer(service_item, mock_song)
729
730 # THEN: The copyright symbol should not be in the footer
731- assert service_item.raw_footer == ['My Song', 'My copyright']
732+ assert service_item.raw_footer == ['My Song', '© My copyright']
733
734 def test_authors_match(self):
735 """