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: 720 lines (+289/-144)
7 files modified
openlp/core/lib/serviceitem.py (+5/-7)
openlp/core/ui/maindisplay.py (+2/-2)
openlp/core/ui/printserviceform.py (+5/-5)
openlp/plugins/songs/lib/mediaitem.py (+43/-19)
openlp/plugins/songs/lib/songstab.py (+74/-37)
openlp/plugins/songs/songsplugin.py (+53/-3)
tests/functional/openlp_plugins/songs/test_mediaitem.py (+107/-71)
To merge this branch: bzr merge lp:~sam92/openlp/bug-1695620
Reviewer Review Type Date Requested Status
Raoul Snyman Disapprove
Tim Bentley Needs Fixing
Review via email: mp+325043@code.launchpad.net

This proposal has been superseded by a proposal from 2017-06-12.

Description of the change

make use of pystache 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 pystache 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 :

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 :

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

Use Mako instead of Pystache

2754. By Samuel Mehrbrodt

Fix test

2755. By Samuel Mehrbrodt

Merge trunk

2756. By Samuel Mehrbrodt

PEP8 fixes

2757. By Samuel Mehrbrodt

Remove blank lines

2758. By Samuel Mehrbrodt

Adjust display of available placeholders to mako syntax

2759. By Samuel Mehrbrodt

Only save footer template if it has been changed

2760. By Samuel Mehrbrodt

Merge trunk

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