From 66c9f8eb82ad160edb688f01be95a1f0c6e2bd54 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 4 Jun 2017 01:31:47 +0200 Subject: [PATCH 01/39] made 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) - adjusted tests to new and removed functionality Fixes: https://launchpad.net/bugs/1695620 --- openlp/core/lib/serviceitem.py | 12 +- openlp/core/ui/maindisplay.py | 4 +- openlp/core/ui/printserviceform.py | 10 +- openlp/plugins/songs/lib/__init__.py | 14 ++ openlp/plugins/songs/lib/mediaitem.py | 50 +++--- openlp/plugins/songs/lib/songstab.py | 106 ++++++++----- openlp/plugins/songs/songsplugin.py | 43 ++++- .../openlp_plugins/songs/test_mediaitem.py | 147 ++++++++++-------- 8 files changed, 251 insertions(+), 135 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index c8040aa58..d107c47e6 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -163,7 +163,8 @@ class ServiceItem(RegistryProperties): self.items = [] self.iconic_representation = None self.raw_footer = [] - self.foot_text = '' + # Plugins can set footer_html themselves. If they don't, it will be generated from raw_footer. + self.footer_html = '' self.theme = None self.service_item_type = None self._raw_frames = [] @@ -276,12 +277,9 @@ class ServiceItem(RegistryProperties): else: log.error('Invalid value renderer: {item}'.format(item=self.service_item_type)) self.title = clean_tags(self.title) - # The footer should never be None, but to be compatible with a few - # nightly builds between 1.9.4 and 1.9.5, we have to correct this to - # avoid tracebacks. - if self.raw_footer is None: - self.raw_footer = [] - self.foot_text = '
'.join([_f for _f in self.raw_footer if _f]) + + if not self.footer_html: + self.footer_html = '
'.join([_f for _f in self.raw_footer if _f]) def add_from_image(self, path, title, background=None, thumbnail=None): """ diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index a40ade826..c2b6b2ffb 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -471,8 +471,8 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): created_html = build_html(self.service_item, self.screen, self.is_live, background, image_bytes, plugins=self.plugin_manager.plugins) self.web_view.setHtml(created_html) - if service_item.foot_text: - self.footer(service_item.foot_text) + if service_item.footer_html: + self.footer(service_item.footer_html) # if was hidden keep it hidden if self.hide_mode and self.is_live and not service_item.is_media(): if Settings().value('core/auto unblank'): diff --git a/openlp/core/ui/printserviceform.py b/openlp/core/ui/printserviceform.py index 7b3d80c8b..b78ddff3c 100644 --- a/openlp/core/ui/printserviceform.py +++ b/openlp/core/ui/printserviceform.py @@ -231,11 +231,11 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert for slide in range(len(item.get_frames())): self._add_element('li', item.get_frame_title(slide), ol) # add footer - foot_text = item.foot_text - foot_text = foot_text.partition('
')[2] - if foot_text: - foot_text = html.escape(foot_text.replace('
', '\n')) - self._add_element('div', foot_text.replace('\n', '
'), parent=div, classId='itemFooter') + footer_html = item.footer_html + footer_html = footer_html.partition('
')[2] + if footer_html: + footer_html = html.escape(footer_html.replace('
', '\n')) + self._add_element('div', footer_html.replace('\n', '
'), parent=div, classId='itemFooter') # Add service items' notes. if self.notes_check_box.isChecked(): if item.notes: diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 6798a39c0..97a1a7a12 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -664,3 +664,17 @@ def transpose_chord(chord, transpose_value, notation): else: transposed_chord += note + rest return transposed_chord + + +def make_list(array): + if len(array) > 0: + result = [] + for i in range(len(array)): + result.append({'entry': array[i]}) + if i == 0: + result[i]['first'] = True + if i == len(array) - 1: + result[i]['last'] = True + return result + else: + return False diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 7c4d128d2..1d944d9c7 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -23,6 +23,7 @@ import logging import os import shutil +import pystache from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_, or_ @@ -36,7 +37,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songexportform import SongExportForm -from openlp.plugins.songs.lib import VerseType, clean_string, delete_song +from openlp.plugins.songs.lib import VerseType, clean_string, delete_song, make_list from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry, Topic from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML @@ -125,9 +126,6 @@ class SongMediaItem(MediaManagerItem): self.is_search_as_you_type_enabled = Settings().value('advanced/search as type') self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit') self.add_song_from_service = Settings().value(self.settings_section + '/add song from service') - self.display_songbook = Settings().value(self.settings_section + '/display songbook') - self.display_written_by_text = Settings().value(self.settings_section + '/display written by') - self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol') def retranslateUi(self): self.search_text_label.setText('{text}:'.format(text=UiStrings().Search)) @@ -641,12 +639,8 @@ class SongMediaItem(MediaManagerItem): item.raw_footer = [] item.raw_footer.append(song.title) if authors_none: - # If the setting for showing "Written by:" is enabled, show it before unspecified authors. - if Settings().value('songs/display written by'): - item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'), - authors=create_separated_list(authors_none))) - else: - item.raw_footer.append("{authors}".format(authors=create_separated_list(authors_none))) + item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'), + authors=create_separated_list(authors_none))) if authors_words_music: item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic], authors=create_separated_list(authors_words_music))) @@ -660,17 +654,35 @@ class SongMediaItem(MediaManagerItem): item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation], authors=create_separated_list(authors_translation))) if song.copyright: - if self.display_copyright_symbol: - item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol, - song=song.copyright)) - else: - item.raw_footer.append(song.copyright) - if self.display_songbook and song.songbook_entries: - songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries] + item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol, + song=song.copyright)) + songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries] + if song.songbook_entries: item.raw_footer.append(", ".join(songbooks)) if Settings().value('core/ccli number'): - item.raw_footer.append(translate('SongsPlugin.MediaItem', - 'CCLI License: ') + Settings().value('core/ccli number')) + item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + + Settings().value('core/ccli number')) + + footer_template = Settings().value('songs/footer template').replace('\n', '').replace(' ', '') + # Keep this in sync with the list in songstab.py + item.footer_html = pystache.render(footer_template, { + 'title': song.title, + 'authors_none_label': translate('OpenLP.Ui', 'Written by'), + 'authors_none': make_list(authors_none), + 'authors_words_label': AuthorType.Types[AuthorType.Words], + 'authors_words': make_list(authors_words), + 'authors_music_label': AuthorType.Types[AuthorType.Music], + 'authors_music': make_list(authors_music), + 'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic], + 'authors_words_music': make_list(authors_words_music), + 'authors_translation_label': AuthorType.Types[AuthorType.Translation], + 'authors_translation': make_list(authors_translation), + 'copyright': song.copyright, + 'songbook_entries': make_list(songbooks), + 'ccli_license': Settings().value('core/ccli number'), + 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License') + }) + return authors_all def service_load(self, item): diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index d1044b6c3..99e6c0efa 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -24,7 +24,7 @@ from PyQt5 import QtCore, QtWidgets from openlp.core.common import Settings, translate from openlp.core.lib import SettingsTab -from openlp.plugins.songs.lib.ui import SongStrings +from openlp.plugins.songs.lib.db import AuthorType class SongsTab(SettingsTab): @@ -50,15 +50,6 @@ class SongsTab(SettingsTab): self.add_from_service_check_box = QtWidgets.QCheckBox(self.mode_group_box) self.add_from_service_check_box.setObjectName('add_from_service_check_box') self.mode_layout.addWidget(self.add_from_service_check_box) - self.display_songbook_check_box = QtWidgets.QCheckBox(self.mode_group_box) - self.display_songbook_check_box.setObjectName('songbook_check_box') - self.mode_layout.addWidget(self.display_songbook_check_box) - self.display_written_by_check_box = QtWidgets.QCheckBox(self.mode_group_box) - self.display_written_by_check_box.setObjectName('written_by_check_box') - self.mode_layout.addWidget(self.display_written_by_check_box) - self.display_copyright_check_box = QtWidgets.QCheckBox(self.mode_group_box) - self.display_copyright_check_box.setObjectName('copyright_check_box') - self.mode_layout.addWidget(self.display_copyright_check_box) self.left_layout.addWidget(self.mode_group_box) # Chords group box self.chords_group_box = QtWidgets.QGroupBox(self.left_column) @@ -89,19 +80,36 @@ class SongsTab(SettingsTab): self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button') self.chords_layout.addWidget(self.neolatin_notation_radio_button) self.left_layout.addWidget(self.chords_group_box) + + # Footer group box + self.footer_group_box = QtWidgets.QGroupBox(self.left_column) + self.footer_group_box.setObjectName('footer_group_box') + self.footer_layout = QtWidgets.QVBoxLayout(self.footer_group_box) + self.footer_layout.setObjectName('chords_layout') + self.footer_info_label = QtWidgets.QLabel(self.footer_group_box) + self.footer_layout.addWidget(self.footer_info_label) + self.footer_placeholder_info = QtWidgets.QTextEdit(self.footer_group_box) + self.footer_layout.addWidget(self.footer_placeholder_info) + self.footer_desc_label = QtWidgets.QLabel(self.footer_group_box) + self.footer_layout.addWidget(self.footer_desc_label) + self.footer_edit_box = QtWidgets.QTextEdit(self.footer_group_box) + self.footer_layout.addWidget(self.footer_edit_box) + self.footer_reset_button = QtWidgets.QPushButton(self.footer_group_box) + self.footer_layout.addWidget(self.footer_reset_button, alignment=QtCore.Qt.AlignRight) + self.right_layout.addWidget(self.footer_group_box) + + self.left_layout.addStretch() self.right_layout.addStretch() self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed) self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed) self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed) - self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed) - self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed) - self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed) self.mainview_chords_check_box.stateChanged.connect(self.on_mainview_chords_check_box_changed) self.disable_chords_import_check_box.stateChanged.connect(self.on_disable_chords_import_check_box_changed) self.english_notation_radio_button.clicked.connect(self.on_english_notation_button_clicked) self.german_notation_radio_button.clicked.connect(self.on_german_notation_button_clicked) self.neolatin_notation_radio_button.clicked.connect(self.on_neolatin_notation_button_clicked) + self.footer_reset_button.clicked.connect(self.on_footer_reset_button_clicked) def retranslateUi(self): self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings')) @@ -110,12 +118,6 @@ class SongsTab(SettingsTab): self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit')) self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab', 'Import missing songs from Service files')) - self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer')) - self.display_written_by_check_box.setText(translate( - 'SongsPlugin.SongsTab', 'Show "Written by:" in footer for unspecified authors')) - self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab', - 'Display "{symbol}" symbol before copyright ' - 'info').format(symbol=SongStrings.CopyrightSymbol)) self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will ' 'be regarded as chords.')) self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords')) @@ -127,6 +129,49 @@ class SongsTab(SettingsTab): self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)') self.neolatin_notation_radio_button.setText( translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)') + self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer')) + + # Keep this in sync with the list in mediaitem.py + const = '"{}"' + placeholders = [ + ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False], + ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False], + ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), True, True], + ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False], + ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type Words)'), True, True], + ['authors_music_label', const.format(AuthorType.Types[AuthorType.Music]), False, False], + ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type Music)'), True, True], + ['authors_words_music_label', const.format(AuthorType.Types[AuthorType.WordsAndMusic]), False, False], + ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type Words and Music)'), True, True], + ['authors_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False], + ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type Translation)'), True, True], + ['copyright', translate('SongsPlugin.SongsTab', 'Copyright information'), True, False], + ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), True, True], + ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False], + ['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False], + ] + + placeholder_info = '\n\n'\ + .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'), + desc=translate('SongsPlugin.SongsTab', 'Description')) + for placeholder in placeholders: + placeholder_info += '\n'\ + .format(pl=placeholder[0], des=placeholder[1], + opt=(' ¹' if placeholder[2] else '') + (' ²' if placeholder[3] else '')) + placeholder_info += '
{ph}{desc}
{{{pl}}}{des}{opt}
' + placeholder_info += '\n
¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty')) + placeholder_info += '\n
² {}:
{{#first}} True {}
{{entry}} {}
' \ + '{{#last}} True {}
'\ + .format(translate('SongsPlugin.SongsTab', 'list of entries'), + translate('SongsPlugin.SongsTab', 'for first element of list'), + translate('SongsPlugin.SongsTab', 'iterates over all entries'), + translate('SongsPlugin.SongsTab', 'for last element')) + self.footer_placeholder_info.setHtml(placeholder_info) + self.footer_placeholder_info.setReadOnly(True) + + self.footer_info_label.setText(translate('SongsPlugin.SongsTab', 'How to use Footers:')) + self.footer_desc_label.setText(translate('SongsPlugin.SongsTab', 'Footer Template (Mustache Syntax):')) + self.footer_reset_button.setText(translate('SongsPlugin.SongsTab', 'Reset Template')) def on_search_as_type_check_box_changed(self, check_state): self.song_search = (check_state == QtCore.Qt.Checked) @@ -140,15 +185,6 @@ class SongsTab(SettingsTab): def on_add_from_service_check_box_changed(self, check_state): self.update_load = (check_state == QtCore.Qt.Checked) - def on_songbook_check_box_changed(self, check_state): - self.display_songbook = (check_state == QtCore.Qt.Checked) - - def on_written_by_check_box_changed(self, check_state): - self.display_written_by = (check_state == QtCore.Qt.Checked) - - def on_copyright_check_box_changed(self, check_state): - self.display_copyright_symbol = (check_state == QtCore.Qt.Checked) - def on_mainview_chords_check_box_changed(self, check_state): self.mainview_chords = (check_state == QtCore.Qt.Checked) @@ -164,15 +200,15 @@ class SongsTab(SettingsTab): def on_neolatin_notation_button_clicked(self): self.chord_notation = 'neo-latin' + def on_footer_reset_button_clicked(self): + self.footer_edit_box.setPlainText(Settings().get_default_value('songs/footer template')); + def load(self): settings = Settings() settings.beginGroup(self.settings_section) self.tool_bar = settings.value('display songbar') self.update_edit = settings.value('update service on edit') self.update_load = settings.value('add song from service') - self.display_songbook = settings.value('display songbook') - self.display_written_by = settings.value('display written by') - self.display_copyright_symbol = settings.value('display copyright symbol') self.enable_chords = settings.value('enable chords') self.chord_notation = settings.value('chord notation') self.mainview_chords = settings.value('mainview chords') @@ -180,9 +216,6 @@ class SongsTab(SettingsTab): self.tool_bar_active_check_box.setChecked(self.tool_bar) self.update_on_edit_check_box.setChecked(self.update_edit) self.add_from_service_check_box.setChecked(self.update_load) - self.display_songbook_check_box.setChecked(self.display_songbook) - self.display_written_by_check_box.setChecked(self.display_written_by) - self.display_copyright_check_box.setChecked(self.display_copyright_symbol) self.chords_group_box.setChecked(self.enable_chords) self.mainview_chords_check_box.setChecked(self.mainview_chords) self.disable_chords_import_check_box.setChecked(self.disable_chords_import) @@ -192,6 +225,7 @@ class SongsTab(SettingsTab): self.neolatin_notation_radio_button.setChecked(True) else: self.english_notation_radio_button.setChecked(True) + self.footer_edit_box.setPlainText(settings.value('footer template')) settings.endGroup() def save(self): @@ -200,13 +234,11 @@ class SongsTab(SettingsTab): settings.setValue('display songbar', self.tool_bar) settings.setValue('update service on edit', self.update_edit) settings.setValue('add song from service', self.update_load) - settings.setValue('display songbook', self.display_songbook) - settings.setValue('display written by', self.display_written_by) - settings.setValue('display copyright symbol', self.display_copyright_symbol) settings.setValue('enable chords', self.chords_group_box.isChecked()) settings.setValue('mainview chords', self.mainview_chords) settings.setValue('disable chords import', self.disable_chords_import) settings.setValue('chord notation', self.chord_notation) + settings.setValue('footer template', self.footer_edit_box.toPlainText()) settings.endGroup() if self.tab_visited: self.settings_form.register_post_process('songs_config_updated') diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 4494ade49..08d9ea9f0 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -59,9 +59,6 @@ __default_settings__ = { 'songs/update service on edit': False, 'songs/add song from service': True, 'songs/display songbar': True, - 'songs/display songbook': False, - 'songs/display written by': True, - 'songs/display copyright symbol': False, 'songs/last directory import': '', 'songs/last directory export': '', 'songs/songselect username': '', @@ -71,6 +68,46 @@ __default_settings__ = { 'songs/chord notation': 'english', # Can be english, german or neo-latin 'songs/mainview chords': False, 'songs/disable chords import': False, + 'songs/footer template': """{{title}}
+ +{{#authors_none}} + {{#first}}{{authors_none_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_none}} +{{#authors_words_music}} + {{#first}}{{authors_words_music_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_words_music}} +{{#authors_words}} + {{#first}}{{authors_words_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_words}} +{{#authors_music}} + {{#first}}{{authors_music_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_music}} +{{#authors_translation}} + {{#first}}{{authors_translation_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_translation}} + +{{#copyright}} + © {{copyright}}
+{{/copyright}} + +{{#songbook_entries}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/songbook_entries}} + +{{#ccli_license}} + {{ccli_license_label}}: {{ccli_license}}
+{{/ccli_license}}""", } diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index d5b06b7dc..557891c71 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -34,6 +34,49 @@ from openlp.plugins.songs.lib.mediaitem import SongMediaItem from tests.helpers.testmixin import TestMixin +__default_settings__ = { + 'songs/footer template': """{{title}}
+ +{{#authors_none}} + {{#first}}{{authors_none_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_none}} +{{#authors_words_music}} + {{#first}}{{authors_words_music_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_words_music}} +{{#authors_words}} + {{#first}}{{authors_words_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_words}} +{{#authors_music}} + {{#first}}{{authors_music_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_music}} +{{#authors_translation}} + {{#first}}{{authors_translation_label}}: {{/first}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/authors_translation}} + +{{#copyright}} + © {{copyright}}
+{{/copyright}} + +{{#songbook_entries}} + {{entry}}{{^last}}, {{/last}} + {{#last}}
{{/last}} +{{/songbook_entries}} + +{{#ccli_license}} + {{ccli_license_label}}: {{ccli_license}}
+{{/ccli_license}}""" +} + class TestMediaItem(TestCase, TestMixin): """ @@ -60,6 +103,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.display_copyright_symbol = False self.setup_application() self.build_settings() + Settings().extend_default_settings(__default_settings__) QtCore.QLocale.setDefault(QtCore.QLocale('en_GB')) def tearDown(self): @@ -304,61 +348,43 @@ class TestMediaItem(TestCase, TestMixin): # and False for 'core/ccli number' (ccli will cause traceback if true) mocked_settings = MagicMock() - mocked_settings.value.side_effect = [True, False] + mocked_settings.value.side_effect = [False, "", "0"] MockedSettings.return_value = mocked_settings - mock_song = MagicMock() - mock_song.title = 'My Song' - mock_song.authors_songs = [] - mock_author = MagicMock() - mock_author.display_name = 'my author' - mock_author_song = MagicMock() - mock_author_song.author = mock_author - mock_song.authors_songs.append(mock_author_song) - mock_song.copyright = 'My copyright' - service_item = ServiceItem(None) + with patch('pystache.Renderer.render') as MockedRenderer: + mock_song = MagicMock() + mock_song.title = 'My Song' + mock_song.authors_songs = [] + mock_author = MagicMock() + mock_author.display_name = 'my author' + mock_author_song = MagicMock() + mock_author_song.author = mock_author + mock_song.authors_songs.append(mock_author_song) + mock_song.copyright = 'My copyright' + mock_song.songbook_entries = [] + service_item = ServiceItem(None) + MockedRenderer.return_value = "" - # WHEN: I generate the Footer with default settings - author_list = self.media_item.generate_footer(service_item, mock_song) + # WHEN: I generate the Footer with default settings + author_list = self.media_item.generate_footer(service_item, mock_song) - # THEN: I get the following Array returned - self.assertEqual(service_item.raw_footer, ['My Song', 'Written by: my author', 'My copyright'], - 'The array should be returned correctly with a song, one author and copyright') - self.assertEqual(author_list, ['my author'], - 'The author list should be returned correctly with one author') + # THEN: I get nothing real returned + self.assertEqual(service_item.footer_html, "", 'pystache isnt in scope') - @patch(u'openlp.plugins.songs.lib.mediaitem.Settings') - def test_build_song_footer_one_author_hide_written_by(self, MockedSettings): - """ - Test build songs footer with basic song and one author - """ - # GIVEN: A Song and a Service Item, mocked settings: False for 'songs/display written by' - # and False for 'core/ccli number' (ccli will cause traceback if true) - - mocked_settings = MagicMock() - mocked_settings.value.side_effect = [False, False] - MockedSettings.return_value = mocked_settings - - mock_song = MagicMock() - mock_song.title = 'My Song' - mock_song.authors_songs = [] - mock_author = MagicMock() - mock_author.display_name = 'my author' - mock_author_song = MagicMock() - mock_author_song.author = mock_author - mock_song.authors_songs.append(mock_author_song) - mock_song.copyright = 'My copyright' - service_item = ServiceItem(None) - - # WHEN: I generate the Footer with default settings - author_list = self.media_item.generate_footer(service_item, mock_song) - - # THEN: I get the following Array returned - self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright'], - 'The array should be returned correctly with a song, one author and copyright,' - 'text Written by should not be part of the text.') - self.assertEqual(author_list, ['my author'], - 'The author list should be returned correctly with one author') + # AND: The psytache function was called with the following arguments + MockedRenderer.assert_called_once_with('', {'authors_music_label': 'Music', + 'authors_none': [{'first': True, 'entry': 'my author', + 'last': True}], 'authors_words_music': False, + 'authors_words': False, 'authors_music': False, + 'authors_translation_label': 'Translation', + 'authors_words_music_label': 'Words and Music', + 'title': 'My Song', 'ccli_license': "0", + 'copyright': 'My copyright', 'authors_none_label': 'Written by', + 'ccli_license_label': 'CCLI License', + 'authors_translation': False, 'authors_words_label': 'Words', + 'songbook_entries': False}) + self.assertEqual(author_list, ['my author'], + 'The author list should be returned correctly with one author') def test_build_song_footer_two_authors(self): """ @@ -387,6 +413,7 @@ class TestMediaItem(TestCase, TestMixin): mock_author_song.author_type = AuthorType.Translation mock_song.authors_songs.append(mock_author_song) mock_song.copyright = 'My copyright' + mock_song.songbook_entries = [] service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings @@ -394,7 +421,7 @@ class TestMediaItem(TestCase, TestMixin): # THEN: I get the following Array returned self.assertEqual(service_item.raw_footer, ['My Song', 'Words: another author', 'Music: my author', - 'Translation: translator', 'My copyright'], + 'Translation: translator', '© My copyright'], 'The array should be returned correctly with a song, two authors and copyright') self.assertEqual(author_list, ['another author', 'my author', 'translator'], 'The author list should be returned correctly with two authors') @@ -407,6 +434,7 @@ class TestMediaItem(TestCase, TestMixin): mock_song = MagicMock() mock_song.title = 'My Song' mock_song.copyright = 'My copyright' + mock_song.songbook_entries = [] service_item = ServiceItem(None) Settings().setValue('core/ccli number', '1234') @@ -414,7 +442,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.generate_footer(service_item, mock_song) # THEN: I get the following Array returned - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 1234'], + self.assertEqual(service_item.raw_footer, ['My Song', '© My copyright', 'CCLI License: 1234'], 'The array should be returned correctly with a song, an author, copyright and ccli') # WHEN: I amend the CCLI value @@ -422,7 +450,7 @@ class TestMediaItem(TestCase, TestMixin): self.media_item.generate_footer(service_item, mock_song) # THEN: I would get an amended footer string - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 4321'], + self.assertEqual(service_item.raw_footer, ['My Song', '© My copyright', 'CCLI License: 4321'], 'The array should be returned correctly with a song, an author, copyright and amended ccli') def test_build_song_footer_base_songbook(self): @@ -448,15 +476,8 @@ class TestMediaItem(TestCase, TestMixin): # WHEN: I generate the Footer with default settings self.media_item.generate_footer(service_item, song) - # THEN: The songbook should not be in the footer - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright']) - - # WHEN: I activate the "display songbook" option - self.media_item.display_songbook = True - self.media_item.generate_footer(service_item, song) - # THEN: The songbook should be in the footer - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']) + self.assertEqual(service_item.raw_footer, ['My Song', '© My copyright', 'My songbook #12, Thy songbook #502A']) def test_build_song_footer_copyright_enabled(self): """ @@ -467,6 +488,7 @@ class TestMediaItem(TestCase, TestMixin): mock_song = MagicMock() mock_song.title = 'My Song' mock_song.copyright = 'My copyright' + mock_song.songbook_entries = [] service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings @@ -483,13 +505,14 @@ class TestMediaItem(TestCase, TestMixin): mock_song = MagicMock() mock_song.title = 'My Song' mock_song.copyright = 'My copyright' + mock_song.songbook_entries = [] service_item = ServiceItem(None) # WHEN: I generate the Footer with default settings self.media_item.generate_footer(service_item, mock_song) # THEN: The copyright symbol should not be in the footer - self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright']) + self.assertEqual(service_item.raw_footer, ['My Song', '© My copyright']) def test_authors_match(self): """ From 88642395ee2ac699dcc21aeea54ce2b1c2d47e1c Mon Sep 17 00:00:00 2001 From: Johannes Thomas Meyer Date: Sun, 4 Jun 2017 02:42:41 +0200 Subject: [PATCH 02/39] added a placeholder to show the alternate title of a song Fixes: https://launchpad.net/bugs/1694430 --- openlp/plugins/songs/lib/mediaitem.py | 1 + openlp/plugins/songs/lib/songstab.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 1d944d9c7..34493dbe2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -667,6 +667,7 @@ class SongMediaItem(MediaManagerItem): # Keep this in sync with the list in songstab.py item.footer_html = pystache.render(footer_template, { 'title': song.title, + 'alternate_title': song.alternate_title, 'authors_none_label': translate('OpenLP.Ui', 'Written by'), 'authors_none': make_list(authors_none), 'authors_words_label': AuthorType.Types[AuthorType.Words], diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 99e6c0efa..ce07eb618 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -134,7 +134,9 @@ class SongsTab(SettingsTab): # Keep this in sync with the list in mediaitem.py const = '"{}"' placeholders = [ + # placeholder, description, can be empty, is a list ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False], + ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), False, False], ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False], ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), True, True], ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False], From 0f3d86f0d1d158ee8d03e0c4564bd0b8c1704c94 Mon Sep 17 00:00:00 2001 From: Johannes Thomas Meyer Date: Sun, 4 Jun 2017 02:53:33 +0200 Subject: [PATCH 03/39] added a placeholder to show the topics of a song - and included the empty-description for lists always --- openlp/plugins/songs/lib/mediaitem.py | 3 ++- openlp/plugins/songs/lib/songstab.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 34493dbe2..d6345a04e 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -681,7 +681,8 @@ class SongMediaItem(MediaManagerItem): 'copyright': song.copyright, 'songbook_entries': make_list(songbooks), 'ccli_license': Settings().value('core/ccli number'), - 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License') + 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), + 'topics': make_list([topic.name for topic in song.topics]) }) return authors_all diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index ce07eb618..b7ae4799e 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -138,19 +138,20 @@ class SongsTab(SettingsTab): ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False], ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), False, False], ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False], - ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), True, True], + ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), False, True], ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False], - ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type Words)'), True, True], + ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type Words)'), False, True], ['authors_music_label', const.format(AuthorType.Types[AuthorType.Music]), False, False], - ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type Music)'), True, True], + ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type Music)'), False, True], ['authors_words_music_label', const.format(AuthorType.Types[AuthorType.WordsAndMusic]), False, False], - ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type Words and Music)'), True, True], + ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type Words and Music)'), False, True], ['authors_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False], - ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type Translation)'), True, True], + ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type Translation)'), False, True], ['copyright', translate('SongsPlugin.SongsTab', 'Copyright information'), True, False], - ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), True, True], + ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), False, True], ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False], ['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False], + ['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True], ] placeholder_info = '\n\n'\ @@ -164,7 +165,7 @@ class SongsTab(SettingsTab): placeholder_info += '\n
¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty')) placeholder_info += '\n
² {}:
{{#first}} True {}
{{entry}} {}
' \ '{{#last}} True {}
'\ - .format(translate('SongsPlugin.SongsTab', 'list of entries'), + .format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty'), translate('SongsPlugin.SongsTab', 'for first element of list'), translate('SongsPlugin.SongsTab', 'iterates over all entries'), translate('SongsPlugin.SongsTab', 'for last element')) From eadabee85e745bcda7506bf81b2e518c0cd04890 Mon Sep 17 00:00:00 2001 From: Johannes Thomas Meyer Date: Sun, 4 Jun 2017 15:10:01 +0200 Subject: [PATCH 04/39] added a placeholder to show the ccli number of a song --- openlp/plugins/songs/lib/mediaitem.py | 1 + openlp/plugins/songs/lib/songstab.py | 1 + 2 files changed, 2 insertions(+) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d6345a04e..799681fc7 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -682,6 +682,7 @@ class SongMediaItem(MediaManagerItem): 'songbook_entries': make_list(songbooks), 'ccli_license': Settings().value('core/ccli number'), 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), + 'ccli_number': song.ccli_number, 'topics': make_list([topic.name for topic in song.topics]) }) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index b7ae4799e..299ca19e6 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -151,6 +151,7 @@ class SongsTab(SettingsTab): ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), False, True], ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False], ['ccli_license_label', const.format(translate('SongsPlugin.SongsTab', 'CCLI License')), False, False], + ['ccli_number', translate('SongsPlugin.SongsTab', 'Song CCLI Number'), True, False], ['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True], ] From 21d131191407023a7cf0fcf6e455c523c2678101 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 4 Jun 2017 15:10:50 +0200 Subject: [PATCH 05/39] corrected placeholder tags in explanation --- openlp/plugins/songs/lib/songstab.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 299ca19e6..fc31101d4 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -159,9 +159,10 @@ class SongsTab(SettingsTab): .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'), desc=translate('SongsPlugin.SongsTab', 'Description')) for placeholder in placeholders: - placeholder_info += '\n'\ + placeholder_info += '\n'\ .format(pl=placeholder[0], des=placeholder[1], - opt=(' ¹' if placeholder[2] else '') + (' ²' if placeholder[3] else '')) + opt=(' ¹' if placeholder[2] else '') + + (' ²' if placeholder[3] else '')) placeholder_info += '
{ph}{desc}
{{{pl}}}{des}{opt}
{{{{{pl}}}}}{des}{opt}
' placeholder_info += '\n
¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty')) placeholder_info += '\n
² {}:
{{#first}} True {}
{{entry}} {}
' \ From f89d7992fa2e75868934d65e696b7b4543e37689 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 4 Jun 2017 15:42:21 +0200 Subject: [PATCH 06/39] PEP8 fixes, cleanup, added two more placeholders for footer template, added last_or_penultimate flag in list --- openlp/plugins/songs/lib/__init__.py | 29 ++++++++++++------- openlp/plugins/songs/lib/mediaitem.py | 2 ++ openlp/plugins/songs/lib/songstab.py | 26 ++++++++++------- scripts/check_dependencies.py | 3 +- setup.cfg | 2 +- .../openlp_plugins/songs/test_mediaitem.py | 25 +++++++++------- 6 files changed, 55 insertions(+), 32 deletions(-) diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index 97a1a7a12..f9bda3100 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -667,14 +667,23 @@ def transpose_chord(chord, transpose_value, notation): def make_list(array): - if len(array) > 0: - result = [] - for i in range(len(array)): - result.append({'entry': array[i]}) - if i == 0: - result[i]['first'] = True - if i == len(array) - 1: - result[i]['last'] = True - return result - else: + """ + converts an ordinary list into a mustache ready dict construct augmented with some information to enable special + formatting features with the first, second to last and last element. + + :param array: input list + :return: mustache ready and augmented dict + """ + if len(array) < 0: return False + + result = [] + for i in range(len(array)): + result.append({'entry': array[i]}) + if i == 0: + result[i]['first'] = True + if i == len(array) - 1: + result[i]['last'] = True + if i == len(array) - 1 or i == len(array) - 2: + result[i]['last_or_penultimate'] = True + return result diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 799681fc7..3943f0a3e 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -678,6 +678,8 @@ class SongMediaItem(MediaManagerItem): 'authors_words_music': make_list(authors_words_music), 'authors_translation_label': AuthorType.Types[AuthorType.Translation], 'authors_translation': make_list(authors_translation), + 'authors_words_all': make_list(authors_words + authors_words_music), + 'authors_music_all': make_list(authors_music + authors_words_music), 'copyright': song.copyright, 'songbook_entries': make_list(songbooks), 'ccli_license': Settings().value('core/ccli number'), diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index fc31101d4..fb7a820cc 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -98,7 +98,6 @@ class SongsTab(SettingsTab): self.footer_layout.addWidget(self.footer_reset_button, alignment=QtCore.Qt.AlignRight) self.right_layout.addWidget(self.footer_group_box) - self.left_layout.addStretch() self.right_layout.addStretch() self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed) @@ -140,13 +139,17 @@ class SongsTab(SettingsTab): ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False], ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), False, True], ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False], - ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type Words)'), False, True], + ['authors_words', translate('SongsPlugin.SongsTab', 'Authors (Type "Words")'), False, True], ['authors_music_label', const.format(AuthorType.Types[AuthorType.Music]), False, False], - ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type Music)'), False, True], + ['authors_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Music")'), False, True], ['authors_words_music_label', const.format(AuthorType.Types[AuthorType.WordsAndMusic]), False, False], - ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type Words and Music)'), False, True], + ['authors_words_music', translate('SongsPlugin.SongsTab', 'Authors (Type "Words and Music")'), False, True], ['authors_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False], - ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type Translation)'), False, True], + ['authors_translation', translate('SongsPlugin.SongsTab', 'Authors (Type "Translation")'), False, True], + ['authors_words_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Words" & "Words and Music")'), + False, True], + ['authors_music_all', translate('SongsPlugin.SongsTab', 'Authors (Type "Music" & "Words and Music")'), + False, True], ['copyright', translate('SongsPlugin.SongsTab', 'Copyright information'), True, False], ['songbook_entries', translate('SongsPlugin.SongsTab', 'Songbook Entries'), False, True], ['ccli_license', translate('SongsPlugin.SongsTab', 'CCLI License'), True, False], @@ -166,16 +169,19 @@ class SongsTab(SettingsTab): placeholder_info += '' placeholder_info += '\n
¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty')) placeholder_info += '\n
² {}:
{{#first}} True {}
{{entry}} {}
' \ - '{{#last}} True {}
'\ + '{{#last}} True {}
{{#last_or_penultimate}} True {}
'\ .format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty'), translate('SongsPlugin.SongsTab', 'for first element of list'), translate('SongsPlugin.SongsTab', 'iterates over all entries'), - translate('SongsPlugin.SongsTab', 'for last element')) + translate('SongsPlugin.SongsTab', 'for last element'), + translate('SongsPlugin.SongsTab', 'for last and second to last element')) self.footer_placeholder_info.setHtml(placeholder_info) self.footer_placeholder_info.setReadOnly(True) - + self.footer_info_label.setText(translate('SongsPlugin.SongsTab', 'How to use Footers:')) - self.footer_desc_label.setText(translate('SongsPlugin.SongsTab', 'Footer Template (Mustache Syntax):')) + self.footer_desc_label.setText('{} ({}):' + .format(translate('SongsPlugin.SongsTab', 'Footer Template'), + translate('SongsPlugin.SongsTab', 'Mustache Syntax'))) self.footer_reset_button.setText(translate('SongsPlugin.SongsTab', 'Reset Template')) def on_search_as_type_check_box_changed(self, check_state): @@ -206,7 +212,7 @@ class SongsTab(SettingsTab): self.chord_notation = 'neo-latin' def on_footer_reset_button_clicked(self): - self.footer_edit_box.setPlainText(Settings().get_default_value('songs/footer template')); + self.footer_edit_box.setPlainText(Settings().get_default_value('songs/footer template')) def load(self): settings = Settings() diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index f2e6ebde2..7dcb45840 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -93,7 +93,8 @@ MODULES = [ 'bs4', 'mako', 'uno', - 'six' + 'six', + 'pystache' ] diff --git a/setup.cfg b/setup.cfg index 0ecc03ae8..1291f1b6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -[pep8] +[pycodestyle] exclude=resources.py,vlc.py max-line-length = 120 ignore = E402,E722 diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 557891c71..9b88a96ad 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -354,6 +354,8 @@ class TestMediaItem(TestCase, TestMixin): with patch('pystache.Renderer.render') as MockedRenderer: mock_song = MagicMock() mock_song.title = 'My Song' + mock_song.alternate_title = '' + mock_song.ccli_number = '' mock_song.authors_songs = [] mock_author = MagicMock() mock_author.display_name = 'my author' @@ -372,17 +374,18 @@ class TestMediaItem(TestCase, TestMixin): self.assertEqual(service_item.footer_html, "", 'pystache isnt in scope') # AND: The psytache function was called with the following arguments - MockedRenderer.assert_called_once_with('', {'authors_music_label': 'Music', - 'authors_none': [{'first': True, 'entry': 'my author', - 'last': True}], 'authors_words_music': False, - 'authors_words': False, 'authors_music': False, - 'authors_translation_label': 'Translation', + MockedRenderer.assert_called_once_with('', {'authors_translation': [], 'authors_music_label': 'Music', + 'copyright': 'My copyright', 'songbook_entries': [], + 'alternate_title': '', 'topics': [], 'authors_music_all': [], + 'authors_words_label': 'Words', 'authors_music': [], + 'authors_words_music': [], 'ccli_number': '', + 'authors_none_label': 'Written by', 'title': 'My Song', 'authors_words_music_label': 'Words and Music', - 'title': 'My Song', 'ccli_license': "0", - 'copyright': 'My copyright', 'authors_none_label': 'Written by', - 'ccli_license_label': 'CCLI License', - 'authors_translation': False, 'authors_words_label': 'Words', - 'songbook_entries': False}) + 'authors_none': [{'last_or_penultimate': True, 'last': True, + 'entry': 'my author', 'first': True}], + 'ccli_license_label': 'CCLI License', 'authors_words': [], + 'ccli_license': '0', 'authors_translation_label': 'Translation', + 'authors_words_all': []}) self.assertEqual(author_list, ['my author'], 'The author list should be returned correctly with one author') @@ -463,6 +466,8 @@ class TestMediaItem(TestCase, TestMixin): song.copyright = 'My copyright' song.authors_songs = [] song.songbook_entries = [] + song.alternate_title = '' + song.topics = [] song.ccli_number = '' book1 = MagicMock() book1.name = "My songbook" From a1e1092b10d68799113c3a1d3ae3190610d8177d Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 12 Jun 2017 19:43:24 +0200 Subject: [PATCH 07/39] Use Mako instead of Pystache --- openlp/plugins/songs/lib/__init__.py | 23 -------- openlp/plugins/songs/lib/mediaitem.py | 37 +++++++----- openlp/plugins/songs/lib/songstab.py | 14 ++--- openlp/plugins/songs/songsplugin.py | 85 +++++++++++++++------------ scripts/check_dependencies.py | 3 +- 5 files changed, 76 insertions(+), 86 deletions(-) diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index f9bda3100..6798a39c0 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -664,26 +664,3 @@ def transpose_chord(chord, transpose_value, notation): else: transposed_chord += note + rest return transposed_chord - - -def make_list(array): - """ - converts an ordinary list into a mustache ready dict construct augmented with some information to enable special - formatting features with the first, second to last and last element. - - :param array: input list - :return: mustache ready and augmented dict - """ - if len(array) < 0: - return False - - result = [] - for i in range(len(array)): - result.append({'entry': array[i]}) - if i == 0: - result[i]['first'] = True - if i == len(array) - 1: - result[i]['last'] = True - if i == len(array) - 1 or i == len(array) - 2: - result[i]['last_or_penultimate'] = True - return result diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 3943f0a3e..b314473f3 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -23,7 +23,7 @@ import logging import os import shutil -import pystache +import mako from PyQt5 import QtCore, QtWidgets from sqlalchemy.sql import and_, or_ @@ -31,13 +31,13 @@ from sqlalchemy.sql import and_, or_ from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ check_item_selected, create_separated_list -from openlp.core.lib.ui import create_widget_action +from openlp.core.lib.ui import create_widget_action, critical_error_message_box from openlp.core.common.languagemanager import get_natural_key from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songexportform import SongExportForm -from openlp.plugins.songs.lib import VerseType, clean_string, delete_song, make_list +from openlp.plugins.songs.lib import VerseType, clean_string, delete_song from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry, Topic from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML @@ -663,30 +663,37 @@ class SongMediaItem(MediaManagerItem): item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value('core/ccli number')) - footer_template = Settings().value('songs/footer template').replace('\n', '').replace(' ', '') + footer_template = Settings().value('songs/footer template') # Keep this in sync with the list in songstab.py - item.footer_html = pystache.render(footer_template, { + vars = { 'title': song.title, 'alternate_title': song.alternate_title, 'authors_none_label': translate('OpenLP.Ui', 'Written by'), - 'authors_none': make_list(authors_none), + 'authors_none': authors_none, 'authors_words_label': AuthorType.Types[AuthorType.Words], - 'authors_words': make_list(authors_words), + 'authors_words': authors_words, 'authors_music_label': AuthorType.Types[AuthorType.Music], - 'authors_music': make_list(authors_music), + 'authors_music': authors_music, 'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic], - 'authors_words_music': make_list(authors_words_music), + 'authors_words_music': authors_words_music, 'authors_translation_label': AuthorType.Types[AuthorType.Translation], - 'authors_translation': make_list(authors_translation), - 'authors_words_all': make_list(authors_words + authors_words_music), - 'authors_music_all': make_list(authors_music + authors_words_music), + 'authors_translation': authors_translation, + 'authors_words_all': authors_words + authors_words_music, + 'authors_music_all': authors_music + authors_words_music, 'copyright': song.copyright, - 'songbook_entries': make_list(songbooks), + 'songbook_entries': songbooks, 'ccli_license': Settings().value('core/ccli number'), 'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'), 'ccli_number': song.ccli_number, - 'topics': make_list([topic.name for topic in song.topics]) - }) + 'topics': [topic.name for topic in song.topics] + } + + try: + item.footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '') + except mako.exceptions.SyntaxException: + log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render()) + critical_error_message_box(message=translate('SongsPlugin.MediaItem', + 'Failed to render Song footer html.\nSee log for details')) return authors_all diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index fb7a820cc..575b77bbd 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -135,7 +135,7 @@ class SongsTab(SettingsTab): placeholders = [ # placeholder, description, can be empty, is a list ['title', translate('SongsPlugin.SongsTab', 'Song Title'), False, False], - ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), False, False], + ['alternate_title', translate('SongsPlugin.SongsTab', 'Alternate Title'), True, False], ['written_by', const.format(translate('SongsPlugin.SongsTab', 'Written By')), True, False], ['authors_none', translate('SongsPlugin.SongsTab', 'Authors when type is not set'), False, True], ['authors_words_label', const.format(AuthorType.Types[AuthorType.Words]), False, False], @@ -168,20 +168,14 @@ class SongsTab(SettingsTab): (' ²' if placeholder[3] else '')) placeholder_info += '' placeholder_info += '\n
¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty')) - placeholder_info += '\n
² {}:
{{#first}} True {}
{{entry}} {}
' \ - '{{#last}} True {}
{{#last_or_penultimate}} True {}
'\ - .format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty'), - translate('SongsPlugin.SongsTab', 'for first element of list'), - translate('SongsPlugin.SongsTab', 'iterates over all entries'), - translate('SongsPlugin.SongsTab', 'for last element'), - translate('SongsPlugin.SongsTab', 'for last and second to last element')) + placeholder_info += '\n
² {}'.format(translate('SongsPlugin.SongsTab', 'list of entries, can be empty')) self.footer_placeholder_info.setHtml(placeholder_info) self.footer_placeholder_info.setReadOnly(True) self.footer_info_label.setText(translate('SongsPlugin.SongsTab', 'How to use Footers:')) - self.footer_desc_label.setText('{} ({}):' + self.footer_desc_label.setText('{} ({}):' .format(translate('SongsPlugin.SongsTab', 'Footer Template'), - translate('SongsPlugin.SongsTab', 'Mustache Syntax'))) + translate('SongsPlugin.SongsTab', 'Mako Syntax'))) self.footer_reset_button.setText(translate('SongsPlugin.SongsTab', 'Reset Template')) def on_search_as_type_check_box_changed(self, check_state): diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 08d9ea9f0..e0f3b208a 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -68,46 +68,59 @@ __default_settings__ = { 'songs/chord notation': 'english', # Can be english, german or neo-latin 'songs/mainview chords': False, 'songs/disable chords import': False, - 'songs/footer template': """{{title}}
+ 'songs/footer template': """\ +${title}
-{{#authors_none}} - {{#first}}{{authors_none_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_none}} -{{#authors_words_music}} - {{#first}}{{authors_words_music_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_words_music}} -{{#authors_words}} - {{#first}}{{authors_words_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_words}} -{{#authors_music}} - {{#first}}{{authors_music_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_music}} -{{#authors_translation}} - {{#first}}{{authors_translation_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_translation}} +%if authors_none: + <% + authors = ", ".join(authors_none) + %> + ${authors_none_label}: ${authors}
+%endif -{{#copyright}} - © {{copyright}}
-{{/copyright}} +%if authors_words_music: + <% + authors = ", ".join(authors_words_music) + %> + ${authors_words_music_label}: ${authors}
+%endif -{{#songbook_entries}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/songbook_entries}} +%if authors_words: + <% + authors = ", ".join(authors_words) + %> + ${authors_words_label}: ${authors}
+%endif -{{#ccli_license}} - {{ccli_license_label}}: {{ccli_license}}
-{{/ccli_license}}""", +%if authors_music: + <% + authors = ", ".join(authors_music) + %> + ${authors_music_label}: ${authors}
+%endif + +%if authors_translation: + <% + authors = ", ".join(authors_translation) + %> + ${authors_translation_label}: ${authors}
+%endif + +%if copyright: + © ${copyright}
+%endif + +%if songbook_entries: + <% + entries = ", ".join(songbook_entries) + %> + ${entries}
+%endif + +%if ccli_license: + ${ccli_license_label} ${ccli_license}
+%endif +""", } diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index 7dcb45840..f2e6ebde2 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -93,8 +93,7 @@ MODULES = [ 'bs4', 'mako', 'uno', - 'six', - 'pystache' + 'six' ] From 5fbcbb30473940e1955e8e141ed631f7cf642a01 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 12 Jun 2017 20:04:17 +0200 Subject: [PATCH 08/39] Fix test --- .../openlp_plugins/songs/test_mediaitem.py | 120 ++++++++++-------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 9b88a96ad..6804acb4d 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -35,46 +35,59 @@ from openlp.plugins.songs.lib.mediaitem import SongMediaItem from tests.helpers.testmixin import TestMixin __default_settings__ = { - 'songs/footer template': """{{title}}
+ 'songs/footer template': """ +${title}
-{{#authors_none}} - {{#first}}{{authors_none_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_none}} -{{#authors_words_music}} - {{#first}}{{authors_words_music_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_words_music}} -{{#authors_words}} - {{#first}}{{authors_words_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_words}} -{{#authors_music}} - {{#first}}{{authors_music_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_music}} -{{#authors_translation}} - {{#first}}{{authors_translation_label}}: {{/first}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/authors_translation}} +%if authors_none: + <% + authors = ", ".join(authors_none) + %> + ${authors_none_label}: ${authors}
+%endif -{{#copyright}} - © {{copyright}}
-{{/copyright}} +%if authors_words_music: + <% + authors = ", ".join(authors_words_music) + %> + ${authors_words_music_label}: ${authors}
+%endif -{{#songbook_entries}} - {{entry}}{{^last}}, {{/last}} - {{#last}}
{{/last}} -{{/songbook_entries}} +%if authors_words: + <% + authors = ", ".join(authors_words) + %> + ${authors_words_label}: ${authors}
+%endif -{{#ccli_license}} - {{ccli_license_label}}: {{ccli_license}}
-{{/ccli_license}}""" +%if authors_music: + <% + authors = ", ".join(authors_music) + %> + ${authors_music_label}: ${authors}
+%endif + +%if authors_translation: + <% + authors = ", ".join(authors_translation) + %> + ${authors_translation_label}: ${authors}
+%endif + +%if copyright: + © ${copyright}
+%endif + +%if songbook_entries: + <% + entries = ", ".join(songbook_entries) + %> + ${entries}
+%endif + +%if ccli_license: + ${ccli_license_label} ${ccli_license}
+%endif +""" } @@ -344,14 +357,13 @@ class TestMediaItem(TestCase, TestMixin): """ Test build songs footer with basic song and one author """ - # GIVEN: A Song and a Service Item, mocked settings: True for 'songs/display written by' - # and False for 'core/ccli number' (ccli will cause traceback if true) + # GIVEN: A Song and a Service Item, mocked settings mocked_settings = MagicMock() mocked_settings.value.side_effect = [False, "", "0"] MockedSettings.return_value = mocked_settings - with patch('pystache.Renderer.render') as MockedRenderer: + with patch('mako.template.Template.render_unicode') as MockedRenderer: mock_song = MagicMock() mock_song.title = 'My Song' mock_song.alternate_title = '' @@ -365,27 +377,23 @@ class TestMediaItem(TestCase, TestMixin): mock_song.copyright = 'My copyright' mock_song.songbook_entries = [] service_item = ServiceItem(None) - MockedRenderer.return_value = "" # WHEN: I generate the Footer with default settings author_list = self.media_item.generate_footer(service_item, mock_song) - # THEN: I get nothing real returned - self.assertEqual(service_item.footer_html, "", 'pystache isnt in scope') - - # AND: The psytache function was called with the following arguments - MockedRenderer.assert_called_once_with('', {'authors_translation': [], 'authors_music_label': 'Music', - 'copyright': 'My copyright', 'songbook_entries': [], - 'alternate_title': '', 'topics': [], 'authors_music_all': [], - 'authors_words_label': 'Words', 'authors_music': [], - 'authors_words_music': [], 'ccli_number': '', - 'authors_none_label': 'Written by', 'title': 'My Song', - 'authors_words_music_label': 'Words and Music', - 'authors_none': [{'last_or_penultimate': True, 'last': True, - 'entry': 'my author', 'first': True}], - 'ccli_license_label': 'CCLI License', 'authors_words': [], - 'ccli_license': '0', 'authors_translation_label': 'Translation', - 'authors_words_all': []}) + # THEN: The mako function was called with the following arguments + args = {'authors_translation': [], 'authors_music_label': 'Music', + 'copyright': 'My copyright', 'songbook_entries': [], + 'alternate_title': '', 'topics': [], 'authors_music_all': [], + 'authors_words_label': 'Words', 'authors_music': [], + 'authors_words_music': [], 'ccli_number': '', + 'authors_none_label': 'Written by', 'title': 'My Song', + 'authors_words_music_label': 'Words and Music', + 'authors_none': ['my author'], + 'ccli_license_label': 'CCLI License', 'authors_words': [], + 'ccli_license': '0', 'authors_translation_label': 'Translation', + 'authors_words_all': []} + MockedRenderer.assert_called_once_with(**args) self.assertEqual(author_list, ['my author'], 'The author list should be returned correctly with one author') From f5f4226891e269dc81cfd26b495d77669b0f151c Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 12 Jun 2017 20:09:50 +0200 Subject: [PATCH 09/39] PEP8 fixes --- openlp/plugins/songs/songsplugin.py | 10 +++++----- setup.cfg | 2 +- .../functional/openlp_plugins/songs/test_mediaitem.py | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 3968a184a..4052265f6 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -73,35 +73,35 @@ ${title}
%if authors_none: <% - authors = ", ".join(authors_none) + authors = ", ".join(authors_none) %> ${authors_none_label}: ${authors}
%endif %if authors_words_music: <% - authors = ", ".join(authors_words_music) + authors = ", ".join(authors_words_music) %> ${authors_words_music_label}: ${authors}
%endif %if authors_words: <% - authors = ", ".join(authors_words) + authors = ", ".join(authors_words) %> ${authors_words_label}: ${authors}
%endif %if authors_music: <% - authors = ", ".join(authors_music) + authors = ", ".join(authors_music) %> ${authors_music_label}: ${authors}
%endif %if authors_translation: <% - authors = ", ".join(authors_translation) + authors = ", ".join(authors_translation) %> ${authors_translation_label}: ${authors}
%endif diff --git a/setup.cfg b/setup.cfg index 1291f1b6a..0ecc03ae8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -[pycodestyle] +[pep8] exclude=resources.py,vlc.py max-line-length = 120 ignore = E402,E722 diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index f5e85dcca..e4f9a5e5c 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -40,35 +40,35 @@ ${title}
%if authors_none: <% - authors = ", ".join(authors_none) + authors = ", ".join(authors_none) %> ${authors_none_label}: ${authors}
%endif %if authors_words_music: <% - authors = ", ".join(authors_words_music) + authors = ", ".join(authors_words_music) %> ${authors_words_music_label}: ${authors}
%endif %if authors_words: <% - authors = ", ".join(authors_words) + authors = ", ".join(authors_words) %> ${authors_words_label}: ${authors}
%endif %if authors_music: <% - authors = ", ".join(authors_music) + authors = ", ".join(authors_music) %> ${authors_music_label}: ${authors}
%endif %if authors_translation: <% - authors = ", ".join(authors_translation) + authors = ", ".join(authors_translation) %> ${authors_translation_label}: ${authors}
%endif From 51e644711d061829cb5bf646d0079fd1762ab98b Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 12 Jun 2017 22:53:08 +0200 Subject: [PATCH 10/39] Remove blank lines --- openlp/core/lib/serviceitem.py | 1 - openlp/plugins/songs/lib/songstab.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index d107c47e6..016109024 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -277,7 +277,6 @@ class ServiceItem(RegistryProperties): else: log.error('Invalid value renderer: {item}'.format(item=self.service_item_type)) self.title = clean_tags(self.title) - if not self.footer_html: self.footer_html = '
'.join([_f for _f in self.raw_footer if _f]) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 575b77bbd..585c4266e 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -129,7 +129,6 @@ class SongsTab(SettingsTab): self.neolatin_notation_radio_button.setText( translate('SongsPlugin.SongsTab', 'Neo-Latin') + ' (Do-Re-Mi-Fa-Sol-La-Si)') self.footer_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Footer')) - # Keep this in sync with the list in mediaitem.py const = '"{}"' placeholders = [ @@ -157,7 +156,6 @@ class SongsTab(SettingsTab): ['ccli_number', translate('SongsPlugin.SongsTab', 'Song CCLI Number'), True, False], ['topics', translate('SongsPlugin.SongsTab', 'Topics'), False, True], ] - placeholder_info = '\n\n'\ .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'), desc=translate('SongsPlugin.SongsTab', 'Description')) From 33e3bcd1d5df7405f0c120e0b3a0e7954782e922 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Tue, 13 Jun 2017 10:17:05 +0200 Subject: [PATCH 11/39] Adjust display of available placeholders to mako syntax --- openlp/plugins/songs/lib/songstab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 585c4266e..77aba8b38 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -160,7 +160,7 @@ class SongsTab(SettingsTab): .format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'), desc=translate('SongsPlugin.SongsTab', 'Description')) for placeholder in placeholders: - placeholder_info += '\n'\ + placeholder_info += '\n'\ .format(pl=placeholder[0], des=placeholder[1], opt=(' ¹' if placeholder[2] else '') + (' ²' if placeholder[3] else '')) From af57ef066b3ec8b0ad7454efc67283fdc0d7acc5 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Tue, 13 Jun 2017 10:24:55 +0200 Subject: [PATCH 12/39] Only save footer template if it has been changed --- openlp/plugins/songs/lib/songstab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 77aba8b38..71e041453 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -241,7 +241,9 @@ class SongsTab(SettingsTab): settings.setValue('mainview chords', self.mainview_chords) settings.setValue('disable chords import', self.disable_chords_import) settings.setValue('chord notation', self.chord_notation) - settings.setValue('footer template', self.footer_edit_box.toPlainText()) + # Only save footer template if it has been changed. This allows future updates + if self.footer_edit_box.toPlainText() != Settings().get_default_value('songs/footer template'): + settings.setValue('footer template', self.footer_edit_box.toPlainText()) settings.endGroup() if self.tab_visited: self.settings_form.register_post_process('songs_config_updated') From 99f0d37253932f08f008e20c5b939b6bbb9d2f68 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Fri, 8 Feb 2019 14:39:40 +0100 Subject: [PATCH 13/39] Remove blank lines --- openlp/plugins/songs/lib/mediaitem.py | 3 --- openlp/plugins/songs/lib/songstab.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 8be8e4b34..dcf30d07f 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -697,7 +697,6 @@ class SongMediaItem(MediaManagerItem): if Settings().value('core/ccli number'): item.raw_footer.append(translate('SongsPlugin.MediaItem', 'CCLI License: ') + Settings().value('core/ccli number')) - footer_template = Settings().value('songs/footer template') # Keep this in sync with the list in songstab.py vars = { @@ -722,14 +721,12 @@ class SongMediaItem(MediaManagerItem): 'ccli_number': song.ccli_number, 'topics': [topic.name for topic in song.topics] } - try: item.footer_html = mako.template.Template(footer_template).render_unicode(**vars).replace('\n', '') except mako.exceptions.SyntaxException: log.error('Failed to render Song footer html:\n' + mako.exceptions.text_error_template().render()) critical_error_message_box(message=translate('SongsPlugin.MediaItem', 'Failed to render Song footer html.\nSee log for details')) - return authors_all def service_load(self, item): diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 7183f6aee..512490938 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -84,7 +84,6 @@ class SongsTab(SettingsTab): self.neolatin_notation_radio_button.setObjectName('neolatin_notation_radio_button') self.chords_layout.addWidget(self.neolatin_notation_radio_button) self.left_layout.addWidget(self.chords_group_box) - # Footer group box self.footer_group_box = QtWidgets.QGroupBox(self.left_column) self.footer_group_box.setObjectName('footer_group_box') @@ -101,7 +100,6 @@ class SongsTab(SettingsTab): self.footer_reset_button = QtWidgets.QPushButton(self.footer_group_box) self.footer_layout.addWidget(self.footer_reset_button, alignment=QtCore.Qt.AlignRight) self.right_layout.addWidget(self.footer_group_box) - self.left_layout.addStretch() self.right_layout.addStretch() self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed) From 94fa2e912a45ef989e3ea945ff2b4dc280b6fb30 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Fri, 22 Feb 2019 08:34:40 +0100 Subject: [PATCH 14/39] Fix songbook as first slide display --- openlp/plugins/songs/lib/mediaitem.py | 8 +++++--- openlp/plugins/songs/lib/songstab.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 96b94c9f7..ff5c96be7 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -585,9 +585,11 @@ class SongMediaItem(MediaManagerItem): if Settings().value('songs/add songbook slide') and song.songbook_entries: first_slide = '\n' for songbook_entry in song.songbook_entries: - first_slide = first_slide + '{book}/{num}/{pub}\n\n'.format(book=songbook_entry.songbook.name, - num=songbook_entry.entry, - pub=songbook_entry.songbook.publisher) + first_slide += '{book} #{num}'.format(book=songbook_entry.songbook.name, + num=songbook_entry.entry) + if songbook_entry.songbook.publisher: + first_slide += ' ({pub})'.format(pub=songbook_entry.songbook.publisher) + first_slide += '\n\n' service_item.add_from_text(first_slide, 'O1') # no verse list or only 1 space (in error) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index 850ce19a2..0e6cfeca9 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -116,7 +116,7 @@ class SongsTab(SettingsTab): self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab', 'Import missing songs from Service files')) self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab', - 'Add Songbooks as first side')) + 'Add Songbooks as first slide')) self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer')) self.display_written_by_check_box.setText(translate( 'SongsPlugin.SongsTab', 'Show "Written by:" in footer for unspecified authors')) From 670c06db60d090b796658340c928896d987e05f0 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 27 Feb 2019 21:12:16 +0000 Subject: [PATCH 15/39] Add proxy settings button to FTW, ftw refactors, a few fixes! --- openlp/core/app.py | 2 +- openlp/core/common/httputils.py | 24 ++- openlp/core/threading.py | 4 +- openlp/core/ui/firsttimeform.py | 169 ++++++++---------- openlp/core/ui/firsttimewizard.py | 119 ++++++------ openlp/core/ui/mainwindow.py | 3 +- openlp/core/widgets/widgets.py | 28 +++ openlp/plugins/songs/lib/importers/openlp.py | 2 +- openlp/plugins/songs/reporting.py | 7 - run_openlp.py | 0 setup.py | 0 .../openlp_core/common/test_httputils.py | 2 +- .../openlp_core/ui/test_firsttimeform.py | 138 ++++++++++++-- 13 files changed, 310 insertions(+), 188 deletions(-) mode change 100755 => 100644 run_openlp.py mode change 100755 => 100644 setup.py diff --git a/openlp/core/app.py b/openlp/core/app.py index 7dafaaf3c..22d0f8195 100644 --- a/openlp/core/app.py +++ b/openlp/core/app.py @@ -101,7 +101,7 @@ class OpenLP(QtWidgets.QApplication): ftw.initialize(screens) if ftw.exec() == QtWidgets.QDialog.Accepted: Settings().setValue('core/has run wizard', True) - elif ftw.was_cancelled: + else: QtCore.QCoreApplication.exit() sys.exit() # Correct stylesheet bugs diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index c9a555a55..1231cb7be 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -158,16 +158,20 @@ def get_web_page(url, headers=None, update_openlp=False, proxy=None): return response.text -def get_url_file_size(url): +def get_url_file_size(url, proxy=None): """ Get the size of a file. :param url: The URL of the file we want to download. + :param dict | ProxyMode | None proxy: ProxyMode enum or a dictionary containing the proxy servers, with their types + as the key e.g. {'http': 'http://proxyserver:port', 'https': 'https://proxyserver:port'} """ retries = 0 + if not isinstance(proxy, dict): + proxy = get_proxy_settings(mode=proxy) while True: try: - response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True) + response = requests.head(url, proxies=proxy, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True) return int(response.headers['Content-Length']) except OSError: if retries > CONNECTION_RETRIES: @@ -178,7 +182,7 @@ def get_url_file_size(url): continue -def download_file(update_object, url, file_path, sha256=None): +def download_file(update_object, url, file_path, sha256=None, proxy=None): """" Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any point. Returns False on download error. @@ -187,15 +191,19 @@ def download_file(update_object, url, file_path, sha256=None): :param url: URL to download :param file_path: Destination file :param sha256: The check sum value to be checked against the download value + :param dict | ProxyMode | None proxy: ProxyMode enum or a dictionary containing the proxy servers, with their types + as the key e.g. {'http': 'http://proxyserver:port', 'https': 'https://proxyserver:port'} """ block_count = 0 block_size = 4096 retries = 0 + if not isinstance(proxy, dict): + proxy = get_proxy_settings(mode=proxy) log.debug('url_get_file: %s', url) while retries < CONNECTION_RETRIES: try: with file_path.open('wb') as saved_file: - response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True) + response = requests.get(url, proxies=proxy, timeout=float(CONNECTION_TIMEOUT), stream=True) if sha256: hasher = hashlib.sha256() # Download until finished or canceled. @@ -244,21 +252,21 @@ class DownloadWorker(ThreadWorker): """ self._base_url = base_url self._file_name = file_name - self._download_cancelled = False + self.was_cancelled = False super().__init__() def start(self): """ Download the url to the temporary directory """ - if self._download_cancelled: + if self.was_cancelled: self.quit.emit() return try: dest_path = Path(gettempdir()) / 'openlp' / self._file_name url = '{url}{name}'.format(url=self._base_url, name=self._file_name) is_success = download_file(self, url, dest_path) - if is_success and not self._download_cancelled: + if is_success and not self.was_cancelled: self.download_succeeded.emit(dest_path) else: self.download_failed.emit() @@ -273,4 +281,4 @@ class DownloadWorker(ThreadWorker): """ A slot to allow the download to be cancelled from outside of the thread """ - self._download_cancelled = True + self.was_cancelled = True diff --git a/openlp/core/threading.py b/openlp/core/threading.py index 502598609..fb51674dd 100644 --- a/openlp/core/threading.py +++ b/openlp/core/threading.py @@ -76,11 +76,11 @@ def get_thread_worker(thread_name): Get the worker by the thread name :param str thread_name: The name of the thread - :returns: The worker for this thread name + :returns: The worker for this thread name, or None """ thread_info = Registry().get('application').worker_threads.get(thread_name) if not thread_info: - raise KeyError('No thread named "{}" exists'.format(thread_name)) + return return thread_info.get('worker') diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index d5f48e2d7..0826a2081 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -46,6 +46,7 @@ from openlp.core.lib.ui import critical_error_message_box from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard from openlp.core.ui.icons import UiIcons +from openlp.core.widgets.widgets import ProxyDialog log = logging.getLogger(__name__) @@ -91,7 +92,7 @@ class ThemeListWidgetItem(QtWidgets.QListWidgetItem): class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ - This is the Theme Import Wizard, which allows easy creation and editing of OpenLP themes. + This is the FirstTimeWizard, designed to help new users to get up and running quickly. """ log.info('ThemeWizardForm loaded') @@ -103,6 +104,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.web_access = True self.web = '' self.setup_ui(self) + self.customButtonClicked.connect(self._on_custom_button_clicked) self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed) self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection) self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll) @@ -111,13 +113,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ Returns the id of the next FirstTimePage to go to based on enabled plugins """ - if FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked(): + if FirstTimePage.Download < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked(): # If the songs plugin is enabled then go to the songs page return FirstTimePage.Songs - elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked(): + elif FirstTimePage.Download < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked(): # Otherwise, if the Bibles plugin is enabled then go to the Bibles page return FirstTimePage.Bibles - elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Themes: + elif FirstTimePage.Download < self.currentId() < FirstTimePage.Themes: # Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes return FirstTimePage.Themes else: @@ -133,9 +135,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): if not self.web_access: return FirstTimePage.NoInternet else: - return FirstTimePage.Plugins - elif self.currentId() == FirstTimePage.Plugins: - return self.get_next_page_id() + return FirstTimePage.Songs elif self.currentId() == FirstTimePage.Progress: return -1 elif self.currentId() == FirstTimePage.NoInternet: @@ -147,7 +147,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): Run the wizard. """ self.set_defaults() - return QtWidgets.QWizard.exec(self) + return super().exec() def initialize(self, screens): """ @@ -227,17 +227,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ self.restart() self.web = 'https://get.openlp.org/ftw/' - self.cancel_button.clicked.connect(self.on_cancel_button_clicked) - self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked) - self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked) self.currentIdChanged.connect(self.on_current_id_changed) Registry().register_function('config_screen_changed', self.screen_selection_widget.load) - self.no_internet_finish_button.setVisible(False) - self.no_internet_cancel_button.setVisible(False) # Check if this is a re-run of the wizard. self.has_run_wizard = Settings().value('core/has run wizard') create_paths(Path(gettempdir(), 'openlp')) self.theme_combo_box.clear() + self.button(QtWidgets.QWizard.CustomButton1).setVisible(False) if self.has_run_wizard: self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active()) self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active()) @@ -260,57 +256,85 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ Detects Page changes and updates as appropriate. """ - # Keep track of the page we are at. Triggering "Cancel" causes page_id to be a -1. + back_button = self.button(QtWidgets.QWizard.BackButton) + cancel_button = self.button(QtWidgets.QWizard.CancelButton) + internet_settings_button = self.button(QtWidgets.QWizard.CustomButton1) + next_button = self.button(QtWidgets.QWizard.NextButton) + back_button.setVisible(True) + next_button.setVisible(True) + internet_settings_button.setVisible(False) self.application.process_events() - if page_id != -1: - self.last_id = page_id - if page_id == FirstTimePage.Download: - self.back_button.setVisible(False) - self.next_button.setVisible(False) - # Set the no internet page text. - if self.has_run_wizard: - self.no_internet_label.setText(self.no_internet_text) - else: - self.no_internet_label.setText(self.no_internet_text + self.cancel_wizard_text) + if page_id == FirstTimePage.SampleOption: + internet_settings_button.setVisible(True) + elif page_id == FirstTimePage.Download: + back_button.setVisible(False) + next_button.setVisible(False) self.application.set_busy_cursor() self._download_index() self.application.set_normal_cursor() - self.back_button.setVisible(False) - self.next_button.setVisible(True) self.next() elif page_id == FirstTimePage.NoInternet: - self.back_button.setVisible(False) - self.next_button.setVisible(False) - self.cancel_button.setVisible(False) - self.no_internet_finish_button.setVisible(True) - if self.has_run_wizard: - self.no_internet_cancel_button.setVisible(False) - else: - self.no_internet_cancel_button.setVisible(True) - elif page_id == FirstTimePage.Plugins: - self.back_button.setVisible(False) + next_button.setVisible(False) + cancel_button.setVisible(False) + internet_settings_button.setVisible(True) elif page_id == FirstTimePage.Progress: + back_button.setVisible(False) + next_button.setVisible(False) self.application.set_busy_cursor() self._pre_wizard() self._perform_wizard() self._post_wizard() self.application.set_normal_cursor() - def on_cancel_button_clicked(self): + def accept(self): """ - Process the triggering of the cancel button. + Called when the user clicks 'Finish'. Reimplement it to to save the plugin status + + :rtype: None + """ + self._set_plugin_status(self.songs_check_box, 'songs/status') + self._set_plugin_status(self.bible_check_box, 'bibles/status') + self._set_plugin_status(self.presentation_check_box, 'presentations/status') + self._set_plugin_status(self.image_check_box, 'images/status') + self._set_plugin_status(self.media_check_box, 'media/status') + self._set_plugin_status(self.custom_check_box, 'custom/status') + self._set_plugin_status(self.song_usage_check_box, 'songusage/status') + self._set_plugin_status(self.alert_check_box, 'alerts/status') + self.screen_selection_widget.save() + if self.theme_combo_box.currentIndex() != -1: + Settings().setValue('themes/global theme', self.theme_combo_box.currentText()) + super().accept() + + def reject(self): + """ + Called when the user clicks the cancel button. Reimplement it to clean up the threads. + + :rtype: None """ self.was_cancelled = True - if self.thumbnail_download_threads: # TODO: Use main thread list - for thread_name in self.thumbnail_download_threads: - worker = get_thread_worker(thread_name) - if worker: - worker.cancel_download() + for thread_name in self.thumbnail_download_threads: + worker = get_thread_worker(thread_name) + if worker: + worker.cancel_download() # Was the thread created. if self.thumbnail_download_threads: while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]): time.sleep(0.1) self.application.set_normal_cursor() + super().reject() + + def _on_custom_button_clicked(self, which): + """ + Slot to handle the a click on one of the wizards custom buttons. + + :param int QtWidgets.QWizard which: The button pressed + :rtype: None + """ + # Internet settings button + if which == QtWidgets.QWizard.CustomButton1: + proxy_dialog = ProxyDialog(self) + proxy_dialog.retranslate_ui() + proxy_dialog.exec() def on_themes_list_widget_selection_changed(self): """ @@ -330,23 +354,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): elif not item.isSelected() and cbox_index != -1: self.theme_combo_box.removeItem(cbox_index) - def on_no_internet_finish_button_clicked(self): - """ - Process the triggering of the "Finish" button on the No Internet page. - """ - self.application.set_busy_cursor() - self._perform_wizard() - self.application.set_normal_cursor() - Settings().setValue('core/has run wizard', True) - self.close() - - def on_no_internet_cancel_button_clicked(self): - """ - Process the triggering of the "Cancel" button on the No Internet page. - """ - self.was_cancelled = True - self.close() - def update_progress(self, count, block_size): """ Calculate and display the download progress. This method is called by download_file(). @@ -373,7 +380,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): Prepare the UI for the process. """ self.max_progress = 0 - self.finish_button.setVisible(False) + self.button(QtWidgets.QWizard.FinishButton).setEnabled(False) self.application.process_events() try: # Loop through the songs list and increase for each selected item @@ -428,58 +435,36 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ Clean up the UI after the process has finished. """ + complete_str = '' if self.max_progress: self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: text = translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the {button} button to return to OpenLP.' - ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText(text) + 'Download complete. Click the \'{finish_button}\' button to return to OpenLP.') else: text = translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the {button} button to start OpenLP.' - ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText(text) + 'Download complete. Click the \'{finish_button}\' button to start OpenLP.') else: if self.has_run_wizard: - text = translate('OpenLP.FirstTimeWizard', - 'Click the {button} button to return to OpenLP.' - ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText(text) + text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to return to OpenLP.') else: - text = translate('OpenLP.FirstTimeWizard', - 'Click the {button} button to start OpenLP.' - ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))) - self.progress_label.setText(text) - self.finish_button.setVisible(True) - self.finish_button.setEnabled(True) - self.cancel_button.setVisible(False) - self.next_button.setVisible(False) + text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to start OpenLP.') + self.progress_label.setText(text.format(finish_button=self.finish_button_text)) + self.button(QtWidgets.QWizard.FinishButton).setEnabled(True) + self.button(QtWidgets.QWizard.CancelButton).setVisible(False) self.application.process_events() def _perform_wizard(self): """ Run the tasks in the wizard. """ - # Set plugin states - self._increment_progress_bar(translate('OpenLP.FirstTimeWizard', 'Enabling selected plugins...')) - self._set_plugin_status(self.songs_check_box, 'songs/status') - self._set_plugin_status(self.bible_check_box, 'bibles/status') - self._set_plugin_status(self.presentation_check_box, 'presentations/status') - self._set_plugin_status(self.image_check_box, 'images/status') - self._set_plugin_status(self.media_check_box, 'media/status') - self._set_plugin_status(self.custom_check_box, 'custom/status') - self._set_plugin_status(self.song_usage_check_box, 'songusage/status') - self._set_plugin_status(self.alert_check_box, 'alerts/status') + if self.web_access: if not self._download_selected(): critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), translate('OpenLP.FirstTimeWizard', 'There was a connection problem while ' 'downloading, so further downloads will be skipped. Try to re-run ' 'the First Time Wizard later.')) - self.screen_selection_widget.save() - if self.theme_combo_box.currentIndex() != -1: - Settings().setValue('themes/global theme', self.theme_combo_box.currentText()) def _download_selected(self): """ diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 37b9389a7..f93ce02f3 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -39,14 +39,15 @@ class FirstTimePage(object): An enumeration class with each of the pages of the wizard. """ Welcome = 0 - ScreenConfig = 1 - Download = 2 - NoInternet = 3 - Plugins = 4 - Songs = 5 - Bibles = 6 - Themes = 7 - Progress = 8 + Plugins = 1 + ScreenConfig = 2 + SampleOption = 3 + Download = 4 + NoInternet = 5 + Songs = 6 + Bibles = 7 + Themes = 8 + Progress = 9 class ThemeListWidget(QtWidgets.QListWidget): @@ -97,20 +98,13 @@ class UiFirstTimeWizard(object): first_time_wizard.resize(550, 386) first_time_wizard.setModal(True) first_time_wizard.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage | - QtWidgets.QWizard.NoBackButtonOnLastPage | QtWidgets.QWizard.HaveCustomButton1 | - QtWidgets.QWizard.HaveCustomButton2) + QtWidgets.QWizard.NoBackButtonOnLastPage | QtWidgets.QWizard.HaveCustomButton1) if is_macosx(): # pragma: nocover first_time_wizard.setPixmap(QtWidgets.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) first_time_wizard.resize(634, 386) else: first_time_wizard.setWizardStyle(QtWidgets.QWizard.ModernStyle) - self.finish_button = self.button(QtWidgets.QWizard.FinishButton) - self.no_internet_finish_button = self.button(QtWidgets.QWizard.CustomButton1) - self.cancel_button = self.button(QtWidgets.QWizard.CancelButton) - self.no_internet_cancel_button = self.button(QtWidgets.QWizard.CustomButton2) - self.next_button = self.button(QtWidgets.QWizard.NextButton) - self.back_button = self.button(QtWidgets.QWizard.BackButton) add_welcome_page(first_time_wizard, ':/wizards/wizard_firsttime.bmp') # The screen config page self.screen_page = QtWidgets.QWizardPage() @@ -121,6 +115,18 @@ class UiFirstTimeWizard(object): self.screen_selection_widget.load() self.screen_page_layout.addRow(self.screen_selection_widget) first_time_wizard.setPage(FirstTimePage.ScreenConfig, self.screen_page) + # Download Samples page + self.resource_page = QtWidgets.QWizardPage() + self.resource_page.setObjectName('resource_page') + self.resource_page.setFinalPage(True) + self.resource_layout = QtWidgets.QVBoxLayout(self.resource_page) + self.resource_layout.setContentsMargins(50, 20, 50, 20) + self.resource_layout.setObjectName('resource_layout') + self.resource_label = QtWidgets.QLabel(self.resource_page) + self.resource_label.setObjectName('resource_label') + self.resource_label.setWordWrap(True) + self.resource_layout.addWidget(self.resource_label) + first_time_wizard.setPage(FirstTimePage.SampleOption, self.resource_page) # The download page self.download_page = QtWidgets.QWizardPage() self.download_page.setObjectName('download_page') @@ -134,6 +140,7 @@ class UiFirstTimeWizard(object): # The "you don't have an internet connection" page. self.no_internet_page = QtWidgets.QWizardPage() self.no_internet_page.setObjectName('no_internet_page') + self.no_internet_page.setFinalPage(True) self.no_internet_layout = QtWidgets.QVBoxLayout(self.no_internet_page) self.no_internet_layout.setContentsMargins(50, 30, 50, 40) self.no_internet_layout.setObjectName('no_internet_layout') @@ -242,27 +249,32 @@ class UiFirstTimeWizard(object): self.progress_bar.setObjectName('progress_bar') self.progress_layout.addWidget(self.progress_bar) first_time_wizard.setPage(FirstTimePage.Progress, self.progress_page) - self.retranslate_ui(first_time_wizard) + self.retranslate_ui() - def retranslate_ui(self, first_time_wizard): + def retranslate_ui(self): """ Translate the UI on the fly :param first_time_wizard: The wizard form """ - first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard')) + self.finish_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton)) + back_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.BackButton)) + next_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.NextButton)) + + self.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard')) text = translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard') - first_time_wizard.title_label.setText('{text}' - ''.format(text=text)) - button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.NextButton)) - first_time_wizard.information_label.setText( + self.title_label.setText('{text}'.format(text=text)) + self.information_label.setText( translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' - 'Click the {button} button below to start.').format(button=button)) + 'Click the \'{next_button}\' button below to start.' + ).format(next_button=next_button_text)) + self.setButtonText( + QtWidgets.QWizard.CustomButton1, translate('OpenLP.FirstTimeWizard', 'Internet Settings')) self.download_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading Resource Index')) - self.download_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while the resource index is ' - 'downloaded.')) - self.download_label.setText(translate('OpenLP.FirstTimeWizard', 'Please wait while OpenLP downloads the ' - 'resource index file...')) + self.download_page.setSubTitle(translate('OpenLP.FirstTimeWizard', + 'Please wait while the resource index is downloaded.')) + self.download_label.setText(translate('OpenLP.FirstTimeWizard', + 'Please wait while OpenLP downloads the resource index file...')) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Select parts of the program you wish to use')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'You can also change these settings after the Wizard.')) @@ -270,11 +282,10 @@ class UiFirstTimeWizard(object): self.screen_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Choose the main display screen for OpenLP.')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) - self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard', - 'Custom Slides – Easier to manage than songs and they have their own' - ' list of slides')) - self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', - 'Bibles – Import and show Bibles')) + self.custom_check_box.setText( + translate('OpenLP.FirstTimeWizard', + 'Custom Slides – Easier to manage than songs and they have their own list of slides')) + self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Bibles – Import and show Bibles')) self.image_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Images – Show images or replace background with them')) self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard', @@ -283,22 +294,25 @@ class UiFirstTimeWizard(object): self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor')) self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Alerts – Display informative messages while showing other slides')) + self.resource_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Resource Data')) + self.resource_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Can OpenLP download some resource data?')) + self.resource_label.setText( + translate('OpenLP.FirstTimeWizard', + 'OpenLP has collected some resources that we have permission to distribute.\n\n' + 'If you would like to download some of these resources click the \'{next_button}\' button, ' + 'otherwise click the \'{finish_button}\' button.' + ).format(next_button=next_button_text, finish_button=self.finish_button_text)) self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection')) - self.no_internet_page.setSubTitle( - translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.')) - button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton)) - self.no_internet_text = translate('OpenLP.FirstTimeWizard', - 'No Internet connection was found. The First Time Wizard needs an Internet ' - 'connection in order to be able to download sample songs, Bibles and themes.' - ' Click the {button} button now to start OpenLP with initial settings and ' - 'no sample data.\n\nTo re-run the First Time Wizard and import this sample ' - 'data at a later time, check your Internet connection and re-run this ' - 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.' - ).format(button=button) - button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.CancelButton)) - self.cancel_wizard_text = translate('OpenLP.FirstTimeWizard', - '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' - 'click the {button} button now.').format(button=button) + self.no_internet_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Cannot connect to the internet.')) + self.no_internet_label.setText( + translate('OpenLP.FirstTimeWizard', + 'OpenLP could not connect to the internet to get information about the sample data available.\n\n' + 'Please check your internet connection. If your church uses a proxy server click the ' + '\'Internet Settings\' button below and enter the server details there.\n\nClick the ' + '\'{back_button}\' button to try again.\n\nIf you click the \'{finish_button}\' ' + 'button you can download the data at a later time by selecting \'Re-run First Time Wizard\' ' + 'from the \'Tools\' menu in OpenLP.' + ).format(back_button=back_button_text, finish_button=self.finish_button_text)) self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs')) self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) @@ -310,13 +324,8 @@ class UiFirstTimeWizard(object): self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all')) self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all')) self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring')) - self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded ' - 'and OpenLP is configured.')) - self.progress_label.setText(translate('OpenLP.FirstTimeWizard', 'Starting configuration process...')) - first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton1, - clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton))) - first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton2, - clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.CancelButton))) + self.progress_page.setSubTitle( + translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded and OpenLP is configured.')) def on_projectors_check_box_clicked(self): # When clicking projectors_check box, change the visibility setting for Projectors panel. diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index dabfca79e..29b131378 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -681,8 +681,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert return first_run_wizard = FirstTimeForm(self) first_run_wizard.initialize(ScreenList()) - first_run_wizard.exec() - if first_run_wizard.was_cancelled: + if first_run_wizard.exec() == QtWidgets.QDialog.Rejected: return self.application.set_busy_cursor() self.first_time() diff --git a/openlp/core/widgets/widgets.py b/openlp/core/widgets/widgets.py index c7697927c..1a193fe08 100644 --- a/openlp/core/widgets/widgets.py +++ b/openlp/core/widgets/widgets.py @@ -150,6 +150,34 @@ class ProxyWidget(QtWidgets.QGroupBox): settings.setValue('advanced/proxy password', self.password_edit.text()) +class ProxyDialog(QtWidgets.QDialog): + """ + A basic dialog to show proxy settingd + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.layout = QtWidgets.QVBoxLayout(self) + self.proxy_widget = ProxyWidget(self) + self.layout.addWidget(self.proxy_widget) + self.button_box = \ + QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self) + self.layout.addWidget(self.button_box) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + def accept(self): + """ + Reimplement the the accept slot so that the ProxyWidget settings can be saved. + :rtype: None + """ + self.proxy_widget.save() + super().accept() + + def retranslate_ui(self): + self.proxy_widget.retranslate_ui() + self.setWindowTitle(translate('OpenLP.ProxyDialog', 'Proxy Server Settings')) + + class ScreenButton(QtWidgets.QPushButton): """ A special button class that holds the screen information about it diff --git a/openlp/plugins/songs/lib/importers/openlp.py b/openlp/plugins/songs/lib/importers/openlp.py index 361406bfd..f6ea3ac0a 100644 --- a/openlp/plugins/songs/lib/importers/openlp.py +++ b/openlp/plugins/songs/lib/importers/openlp.py @@ -106,7 +106,7 @@ class OpenLPSongImport(SongImport): pass # Check the file type - if not isinstance(self.import_source, str) or not self.import_source.endswith('.sqlite'): + if self.import_source.suffix != '.sqlite': self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport', 'Not a valid OpenLP 2 song database.')) return diff --git a/openlp/plugins/songs/reporting.py b/openlp/plugins/songs/reporting.py index 985b9bc12..35521dbe9 100644 --- a/openlp/plugins/songs/reporting.py +++ b/openlp/plugins/songs/reporting.py @@ -49,13 +49,6 @@ def report_song_list(): Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')), translate('SongPlugin.ReportSongList', 'CSV format (*.csv)')) - if report_file_path is None: - main_window.error_message( - translate('SongPlugin.ReportSongList', 'Output Path Not Selected'), - translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n' - 'Please select an existing path on your computer.') - ) - return report_file_path.with_suffix('.csv') Registry().get('application').set_busy_cursor() try: diff --git a/run_openlp.py b/run_openlp.py old mode 100755 new mode 100644 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 diff --git a/tests/functional/openlp_core/common/test_httputils.py b/tests/functional/openlp_core/common/test_httputils.py index bb16e6800..f059d893c 100644 --- a/tests/functional/openlp_core/common/test_httputils.py +++ b/tests/functional/openlp_core/common/test_httputils.py @@ -224,7 +224,7 @@ class TestHttpUtils(TestCase, TestMixin): file_size = get_url_file_size(fake_url) # THEN: The correct methods are called with the correct arguments and a web page is returned - mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, timeout=30.0) + mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, proxies=None, timeout=30.0) assert file_size == 100 @patch('openlp.core.common.httputils.requests') diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index 220e1fff6..e5d0ce550 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -27,6 +27,8 @@ import tempfile from unittest import TestCase from unittest.mock import MagicMock, call, patch, DEFAULT +from PyQt5 import QtWidgets + from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem @@ -120,10 +122,23 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: The screens should be set up, and the default values initialised assert expected_screens == frw.screens, 'The screens should be correct' assert frw.web_access is True, 'The default value of self.web_access should be True' - assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False' assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty' assert frw.has_run_wizard is False, 'has_run_wizard should be False' + @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.exec') + def test_exec(self, mocked_qwizard_exec): + + # GIVEN: An instance of FirstTimeForm + frw = FirstTimeForm(None) + with patch.object(frw, 'set_defaults') as mocked_set_defaults: + + # WHEN: exec is called + frw.exec() + + # THEN: The wizard should be reset and the exec methon on the super class should have been called + mocked_set_defaults.assert_called_once() + mocked_qwizard_exec.assert_called_once() + def test_set_defaults(self): """ Test that the default values are set when set_defaults() is run @@ -134,8 +149,6 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_settings = MagicMock() mocked_settings.value.side_effect = lambda key: {'core/has run wizard': False}[key] with patch.object(frw, 'restart') as mocked_restart, \ - patch.object(frw, 'cancel_button') as mocked_cancel_button, \ - patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \ patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \ patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \ patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \ @@ -153,12 +166,8 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: The default values should have been set mocked_restart.assert_called_once() assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set' - mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked) - mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with( - frw.on_no_internet_finish_button_clicked) mocked_currentIdChanged.connect.assert_called_once_with(frw.on_current_id_changed) mocked_register_function.assert_called_once_with('config_screen_changed', frw.screen_selection_widget.load) - mocked_no_internet_finish_btn.setVisible.assert_called_once_with(False) mocked_settings.value.assert_has_calls([call('core/has run wizard')]) mocked_gettempdir.assert_called_once() mocked_create_paths.assert_called_once_with(Path('temp', 'openlp')) @@ -177,8 +186,6 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_settings.value.side_effect = \ lambda key: {'core/has run wizard': True, 'themes/global theme': 'Default Theme'}[key] with patch.object(frw, 'restart') as mocked_restart, \ - patch.object(frw, 'cancel_button') as mocked_cancel_button, \ - patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \ patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \ patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \ patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT, @@ -200,12 +207,8 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: The default values should have been set mocked_restart.assert_called_once() assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set' - mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked) - mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with( - frw.on_no_internet_finish_button_clicked) mocked_currentIdChanged.connect.assert_called_once_with(frw.on_current_id_changed) mocked_register_function.assert_called_once_with('config_screen_changed', frw.screen_selection_widget.load) - mocked_no_internet_finish_btn.setVisible.assert_called_once_with(False) mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')]) mocked_gettempdir.assert_called_once() mocked_create_paths.assert_called_once_with(Path('temp', 'openlp')) @@ -219,12 +222,79 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_theme_combo_box.findText.assert_called_once_with('Default Theme') mocked_theme_combo_box.setCurrentIndex(3) + @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.accept') + @patch('openlp.core.ui.firsttimewizard.Settings') + def test_accept_method(self, mocked_settings, mocked_qwizard_accept): + """ + Test the FirstTimeForm.accept method + """ + # GIVEN: An instance of FirstTimeForm + frw = FirstTimeForm(None) + with patch.object(frw, '_set_plugin_status') as mocked_set_plugin_status, \ + patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT, + image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT, + song_usage_check_box=DEFAULT, alert_check_box=DEFAULT) as mocked_check_boxes, \ + patch.object(frw, 'screen_selection_widget') as mocked_screen_selection_widget: + + # WHEN: Calling accept + frw.accept() + + # THEN: The selected plugins should be enabled, the screen selection saved and the super method called + mocked_set_plugin_status.assert_has_calls([ + call(mocked_check_boxes['songs_check_box'], 'songs/status'), + call(mocked_check_boxes['bible_check_box'], 'bibles/status'), + call(mocked_check_boxes['presentation_check_box'], 'presentations/status'), + call(mocked_check_boxes['image_check_box'], 'images/status'), + call(mocked_check_boxes['media_check_box'], 'media/status'), + call(mocked_check_boxes['custom_check_box'], 'custom/status'), + call(mocked_check_boxes['song_usage_check_box'], 'songusage/status'), + call(mocked_check_boxes['alert_check_box'], 'alerts/status')]) + mocked_screen_selection_widget.save.assert_called_once() + mocked_qwizard_accept.assert_called_once() + + @patch('openlp.core.ui.firsttimewizard.Settings') + def test_accept_method_theme_not_selected(self, mocked_settings): + """ + Test the FirstTimeForm.accept method when there is no default theme selected + """ + # GIVEN: An instance of FirstTimeForm + frw = FirstTimeForm(None) + with patch.object(frw, '_set_plugin_status'), \ + patch.object(frw, 'screen_selection_widget'), \ + patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': '-1'}): + + # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1 + frw.accept() + + # THEN: OpenLP should not try to save a theme name + mocked_settings.setValue.assert_not_called() + + @patch('openlp.core.ui.firsttimeform.Settings') + def test_accept_method_theme_selected(self, mocked_settings): + """ + Test the FirstTimeForm.accept method when a default theme is selected + """ + # GIVEN: An instance of FirstTimeForm + frw = FirstTimeForm(None) + with patch.object(frw, '_set_plugin_status'), \ + patch.object(frw, 'screen_selection_widget'), \ + patch.object( + frw, 'theme_combo_box', **{'currentIndex.return_value': 0, 'currentText.return_value': 'Test Item'}): + + # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns 0 + frw.accept() + + # THEN: The 'currentItem' in the combobox should have been set as the default theme. + mocked_settings().setValue.assert_called_once_with('themes/global theme', 'Test Item') + + @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.reject') @patch('openlp.core.ui.firsttimeform.time') @patch('openlp.core.ui.firsttimeform.get_thread_worker') @patch('openlp.core.ui.firsttimeform.is_thread_finished') - def test_on_cancel_button_clicked(self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time): + def test_reject_method( + self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time, mocked_qwizard_reject): """ - Test that the cancel button click slot shuts down the threads correctly + Test that the reject method shuts down the threads correctly """ # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff mocked_worker = MagicMock() @@ -235,17 +305,47 @@ class TestFirstTimeForm(TestCase, TestMixin): frw.thumbnail_download_threads = ['test_thread'] with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor: - # WHEN: on_cancel_button_clicked() is called - frw.on_cancel_button_clicked() + # WHEN: the reject method is called + frw.reject() # THEN: The right things should be called in the right order - assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True' mocked_get_thread_worker.assert_called_once_with('test_thread') mocked_worker.cancel_download.assert_called_once() mocked_is_thread_finished.assert_called_with('test_thread') assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice' mocked_time.sleep.assert_called_once_with(0.1) - mocked_set_normal_cursor.assert_called_once_with() + mocked_set_normal_cursor.assert_called_once() + mocked_qwizard_reject.assert_called_once() + + @patch('openlp.core.ui.firsttimeform.ProxyDialog') + def test_on_custom_button_clicked(self, mocked_proxy_dialog): + """ + Test _on_custom_button when it is called whe the 'internet settings' (CustomButton1) button is not clicked. + """ + # GIVEN: An instance of the FirstTimeForm + frw = FirstTimeForm(None) + + # WHEN: Calling _on_custom_button_clicked with a different button to the 'internet settings button. + frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton2) + + # THEN: The ProxyDialog should not be shown. + mocked_proxy_dialog.assert_not_called() + + @patch('openlp.core.ui.firsttimeform.ProxyDialog') + def test_on_custom_button_clicked_internet_settings(self, mocked_proxy_dialog): + """ + Test _on_custom_button when it is called when the 'internet settings' (CustomButton1) button is clicked. + """ + # GIVEN: An instance of the FirstTimeForm + frw = FirstTimeForm(None) + + # WHEN: Calling _on_custom_button_clicked with the constant for the 'internet settings' button (CustomButton1) + frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton1) + + # THEN: The ProxyDialog should be shown. + mocked_proxy_dialog.assert_called_with(frw) + mocked_proxy_dialog().retranslate_ui.assert_called_once() + mocked_proxy_dialog().exec.assert_called_once() @patch('openlp.core.ui.firsttimeform.critical_error_message_box') def test__parse_config_invalid_config(self, mocked_critical_error_message_box): From f3485513f1aa2eb9719b7b6f8456ebf5485b52ff Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Wed, 27 Feb 2019 22:14:08 +0000 Subject: [PATCH 16/39] Test and PEP8 fixes --- tests/functional/openlp_core/test_threading.py | 14 +++++--------- .../openlp_plugins/songs/test_openlpimporter.py | 12 ++++++------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/tests/functional/openlp_core/test_threading.py b/tests/functional/openlp_core/test_threading.py index 6926b1730..e493725d0 100644 --- a/tests/functional/openlp_core/test_threading.py +++ b/tests/functional/openlp_core/test_threading.py @@ -133,15 +133,11 @@ def test_get_thread_worker_mising(MockRegistry): # GIVEN: A mocked thread worker MockRegistry.return_value.get.return_value.worker_threads = {} - try: - # WHEN: get_thread_worker() is called - get_thread_worker('test_thread') - assert False, 'A KeyError should have been raised' - except KeyError: - # THEN: The mocked worker is returned - pass - except Exception: - assert False, 'A KeyError should have been raised' + # WHEN: get_thread_worker() is called + result = get_thread_worker('test_thread') + + # THEN: None should have been returned + assert result is None @patch('openlp.core.threading.Registry') diff --git a/tests/functional/openlp_plugins/songs/test_openlpimporter.py b/tests/functional/openlp_plugins/songs/test_openlpimporter.py index e76c08d3b..c67e18bbd 100644 --- a/tests/functional/openlp_plugins/songs/test_openlpimporter.py +++ b/tests/functional/openlp_plugins/songs/test_openlpimporter.py @@ -22,6 +22,7 @@ """ This module contains tests for the OpenLP song importer. """ +from pathlib import Path from unittest import TestCase from unittest.mock import MagicMock, patch @@ -66,10 +67,9 @@ class TestOpenLPImport(TestCase): importer.stop_import_flag = True # WHEN: Import source is not a list - for source in ['not a list', 0]: - importer.import_source = source + importer.import_source = Path() - # THEN: do_import should return none and the progress bar maximum should not be set. - assert importer.do_import() is None, 'do_import should return None when import_source is not a list' - assert mocked_import_wizard.progress_bar.setMaximum.called is False, \ - 'setMaximum on import_wizard.progress_bar should not have been called' + # THEN: do_import should return none and the progress bar maximum should not be set. + assert importer.do_import() is None, 'do_import should return None when import_source is not a list' + assert mocked_import_wizard.progress_bar.setMaximum.called is False, \ + 'setMaximum on import_wizard.progress_bar should not have been called' From fc8255d6582f48c219083d0e4fcbfb9458489bee Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 8 Mar 2019 21:00:16 +0000 Subject: [PATCH 17/39] PEP fixes --- openlp/core/display/render.py | 4 ++-- .../openlp_core/ui/test_firsttimeform.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openlp/core/display/render.py b/openlp/core/display/render.py index 35f144ee8..0358c74aa 100644 --- a/openlp/core/display/render.py +++ b/openlp/core/display/render.py @@ -43,8 +43,8 @@ from openlp.core.lib.formattingtags import FormattingTags log = logging.getLogger(__name__) SLIM_CHARS = 'fiíIÍjlĺľrtť.,;/ ()|"\'!:\\' -CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)' # noqa - '([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?') +CHORD_LINE_MATCH = re.compile(r'\[(.*?)\]([\u0080-\uFFFF,\w]*)' + r'([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(\Z)?') CHORD_TEMPLATE = '{chord}' FIRST_CHORD_TEMPLATE = '{chord}' CHORD_LINE_TEMPLATE = '{chord}{tail}{whitespace}{remainder}' diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index e5d0ce550..59070f8fd 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -47,6 +47,7 @@ class TestThemeListWidgetItem(TestCase): """ Test the :class:`ThemeListWidgetItem` class """ + def setUp(self): self.sample_theme_data = {'file_name': 'BlueBurst.otz', 'sha256': 'sha_256_hash', 'thumbnail': 'BlueBurst.png', 'title': 'Blue Burst'} @@ -61,7 +62,7 @@ class TestThemeListWidgetItem(TestCase): """ Test that the theme data is loaded correctly in to a ThemeListWidgetItem object when instantiated """ - # GIVEN: A sample theme dictanary object + # GIVEN: A sample theme dictionary object # WHEN: Creating an instance of `ThemeListWidgetItem` instance = ThemeListWidgetItem('url', self.sample_theme_data, MagicMock()) @@ -76,7 +77,7 @@ class TestThemeListWidgetItem(TestCase): """ Test that the `DownloadWorker` worker is set up correctly and that the thread is started. """ - # GIVEN: A sample theme dictanary object + # GIVEN: A sample theme dictionary object mocked_ftw = MagicMock(spec=FirstTimeForm) mocked_ftw.thumbnail_download_threads = [] @@ -259,8 +260,7 @@ class TestFirstTimeForm(TestCase, TestMixin): """ # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) - with patch.object(frw, '_set_plugin_status'), \ - patch.object(frw, 'screen_selection_widget'), \ + with patch.object(frw, '_set_plugin_status'), patch.object(frw, 'screen_selection_widget'), \ patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': '-1'}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1 @@ -277,9 +277,9 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) with patch.object(frw, '_set_plugin_status'), \ - patch.object(frw, 'screen_selection_widget'), \ - patch.object( - frw, 'theme_combo_box', **{'currentIndex.return_value': 0, 'currentText.return_value': 'Test Item'}): + patch.object(frw, 'screen_selection_widget'), \ + patch.object( + frw, 'theme_combo_box', **{'currentIndex.return_value': 0, 'currentText.return_value': 'Test Item'}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns 0 frw.accept() @@ -379,8 +379,8 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: the critical_error_message_box should have been called mocked_message_box.critical.assert_called_once_with( - first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve ' - 'initial configuration information', 'OK') + first_time_form, 'Network Error', + 'There was a network error attempting to connect to retrieve initial configuration information', 'OK') @patch('openlp.core.ui.firsttimewizard.Settings') def test_on_projectors_check_box_checked(self, MockSettings): From 11aa69c9ac38a6e51549dceb5d39a296eee759ab Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 8 Mar 2019 21:25:16 +0000 Subject: [PATCH 18/39] PEP fixes --- openlp/core/common/httputils.py | 2 +- openlp/core/ui/firsttimeform.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 1231cb7be..ba4326f6e 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -203,7 +203,7 @@ def download_file(update_object, url, file_path, sha256=None, proxy=None): while retries < CONNECTION_RETRIES: try: with file_path.open('wb') as saved_file: - response = requests.get(url, proxies=proxy, timeout=float(CONNECTION_TIMEOUT), stream=True) + response = requests.get(url, proxies=proxy, timeout=float(CONNECTION_TIMEOUT), stream=True) if sha256: hasher = hashlib.sha256() # Download until finished or canceled. diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 0826a2081..094403135 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -32,7 +32,7 @@ from tempfile import gettempdir from PyQt5 import QtCore, QtWidgets -from openlp.core.common import clean_button_text, trace_error_handler +from openlp.core.common import trace_error_handler from openlp.core.common.applocation import AppLocation from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page from openlp.core.common.i18n import translate @@ -435,7 +435,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ Clean up the UI after the process has finished. """ - complete_str = '' if self.max_progress: self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: From 1523d9ffa6119d23735c9db955e275cb0d539d00 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 8 Mar 2019 22:51:27 +0000 Subject: [PATCH 19/39] mac test fix? --- tests/functional/openlp_core/ui/test_firsttimeform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index 59070f8fd..d4ddcae46 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -261,6 +261,7 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) with patch.object(frw, '_set_plugin_status'), patch.object(frw, 'screen_selection_widget'), \ + patch.object(frw, 'setup_ui'), \ patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': '-1'}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1 From 87216707d3e45fc714851ca68512fae705e4bd77 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sat, 9 Mar 2019 06:58:52 +0000 Subject: [PATCH 20/39] Test fixes --- openlp/core/ui/firsttimeform.py | 7 +++++ openlp/core/ui/firsttimewizard.py | 7 ----- .../openlp_core/common/test_httputils.py | 12 +++------ .../openlp_core/ui/test_firsttimeform.py | 26 +++++++++---------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 094403135..3af8f66b7 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -336,6 +336,13 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): proxy_dialog.retranslate_ui() proxy_dialog.exec() + def on_projectors_check_box_clicked(self): + # When clicking projectors_check box, change the visibility setting for Projectors panel. + if Settings().value('projector/show after wizard'): + Settings().setValue('projector/show after wizard', False) + else: + Settings().setValue('projector/show after wizard', True) + def on_themes_list_widget_selection_changed(self): """ Update the `theme_combo_box` with the selected items diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index f93ce02f3..d80fb6013 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -326,10 +326,3 @@ class UiFirstTimeWizard(object): self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring')) self.progress_page.setSubTitle( translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded and OpenLP is configured.')) - - def on_projectors_check_box_clicked(self): - # When clicking projectors_check box, change the visibility setting for Projectors panel. - if Settings().value('projector/show after wizard'): - Settings().setValue('projector/show after wizard', False) - else: - Settings().setValue('projector/show after wizard', True) diff --git a/tests/functional/openlp_core/common/test_httputils.py b/tests/functional/openlp_core/common/test_httputils.py index f059d893c..ba1e36441 100644 --- a/tests/functional/openlp_core/common/test_httputils.py +++ b/tests/functional/openlp_core/common/test_httputils.py @@ -249,34 +249,30 @@ class TestGetProxySettings(TestCase, TestMixin): self.addCleanup(self.destroy_settings) @patch('openlp.core.common.httputils.Settings') - def test_mode_arg_specified(self, MockSettings): + def test_mode_arg_specified(self, mocked_settings): """ Test that the argument is used rather than reading the 'advanced/proxy mode' setting """ # GIVEN: Mocked settings - mocked_settings = MagicMock() - MockSettings.return_value = mocked_settings # WHEN: Calling `get_proxy_settings` with the mode arg specified get_proxy_settings(mode=ProxyMode.NO_PROXY) # THEN: The mode arg should have been used rather than looking it up in the settings - mocked_settings.value.assert_not_called() + mocked_settings().value.assert_not_called() @patch('openlp.core.common.httputils.Settings') - def test_mode_incorrect_arg_specified(self, MockSettings): + def test_mode_incorrect_arg_specified(self, mocked_settings): """ Test that the system settings are used when the mode arg specieied is invalid """ # GIVEN: Mocked settings - mocked_settings = MagicMock() - MockSettings.return_value = mocked_settings # WHEN: Calling `get_proxy_settings` with an invalid mode arg specified result = get_proxy_settings(mode='qwerty') # THEN: An None should be returned - mocked_settings.value.assert_not_called() + mocked_settings().value.assert_not_called() assert result is None def test_no_proxy_mode(self): diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index d4ddcae46..36ed165a6 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -126,7 +126,7 @@ class TestFirstTimeForm(TestCase, TestMixin): assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty' assert frw.has_run_wizard is False, 'has_run_wizard should be False' - @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.exec') + @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.exec') def test_exec(self, mocked_qwizard_exec): # GIVEN: An instance of FirstTimeForm @@ -223,9 +223,10 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_theme_combo_box.findText.assert_called_once_with('Default Theme') mocked_theme_combo_box.setCurrentIndex(3) - @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.accept') - @patch('openlp.core.ui.firsttimewizard.Settings') - def test_accept_method(self, mocked_settings, mocked_qwizard_accept): + + @patch('openlp.core.ui.firsttimeform.Settings') + @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.accept') + def test_accept_method(self, mocked_qwizard_accept, *args): """ Test the FirstTimeForm.accept method """ @@ -253,7 +254,7 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_screen_selection_widget.save.assert_called_once() mocked_qwizard_accept.assert_called_once() - @patch('openlp.core.ui.firsttimewizard.Settings') + @patch('openlp.core.ui.firsttimeform.Settings') def test_accept_method_theme_not_selected(self, mocked_settings): """ Test the FirstTimeForm.accept method when there is no default theme selected @@ -261,14 +262,13 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: An instance of FirstTimeForm frw = FirstTimeForm(None) with patch.object(frw, '_set_plugin_status'), patch.object(frw, 'screen_selection_widget'), \ - patch.object(frw, 'setup_ui'), \ - patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': '-1'}): + patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': -1}): # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1 frw.accept() # THEN: OpenLP should not try to save a theme name - mocked_settings.setValue.assert_not_called() + mocked_settings().setValue.assert_not_called() @patch('openlp.core.ui.firsttimeform.Settings') def test_accept_method_theme_selected(self, mocked_settings): @@ -288,7 +288,7 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: The 'currentItem' in the combobox should have been set as the default theme. mocked_settings().setValue.assert_called_once_with('themes/global theme', 'Test Item') - @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.reject') + @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.reject') @patch('openlp.core.ui.firsttimeform.time') @patch('openlp.core.ui.firsttimeform.get_thread_worker') @patch('openlp.core.ui.firsttimeform.is_thread_finished') @@ -383,7 +383,7 @@ class TestFirstTimeForm(TestCase, TestMixin): first_time_form, 'Network Error', 'There was a network error attempting to connect to retrieve initial configuration information', 'OK') - @patch('openlp.core.ui.firsttimewizard.Settings') + @patch('openlp.core.ui.firsttimeform.Settings') def test_on_projectors_check_box_checked(self, MockSettings): """ Test that the projector panel is shown when the checkbox in the first time wizard is checked @@ -398,10 +398,10 @@ class TestFirstTimeForm(TestCase, TestMixin): frw.on_projectors_check_box_clicked() # THEN: The visibility of the projects panel should have been set - mocked_settings.value.assert_called_once_with('projector/show after wizard') - mocked_settings.setValue.assert_called_once_with('projector/show after wizard', False) + mocked_settings().value.assert_called_once_with('projector/show after wizard') + mocked_settings().setValue.assert_called_once_with('projector/show after wizard', False) - @patch('openlp.core.ui.firsttimewizard.Settings') + @patch('openlp.core.ui.firsttimeform.Settings') def test_on_projectors_check_box_unchecked(self, MockSettings): """ Test that the projector panel is shown when the checkbox in the first time wizard is checked From 0578ab8908417491fa119e99b5ae63a95c4d7878 Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Sat, 9 Mar 2019 07:06:00 +0000 Subject: [PATCH 21/39] Test fix --- tests/functional/openlp_core/ui/test_firsttimeform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index 36ed165a6..0a5fe5a0f 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -398,8 +398,8 @@ class TestFirstTimeForm(TestCase, TestMixin): frw.on_projectors_check_box_clicked() # THEN: The visibility of the projects panel should have been set - mocked_settings().value.assert_called_once_with('projector/show after wizard') - mocked_settings().setValue.assert_called_once_with('projector/show after wizard', False) + mocked_settings.value.assert_called_once_with('projector/show after wizard') + mocked_settings.setValue.assert_called_once_with('projector/show after wizard', False) @patch('openlp.core.ui.firsttimeform.Settings') def test_on_projectors_check_box_unchecked(self, MockSettings): From 056b902cf9549969e193d5ba59bf16473a9f9baa Mon Sep 17 00:00:00 2001 From: Philip Ridout Date: Fri, 15 Mar 2019 20:47:47 +0000 Subject: [PATCH 22/39] PEP8 --- openlp/core/ui/firsttimewizard.py | 1 - tests/functional/openlp_core/ui/test_firsttimeform.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index d80fb6013..b3aa042a4 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -26,7 +26,6 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import clean_button_text, is_macosx from openlp.core.common.i18n import translate -from openlp.core.common.settings import Settings from openlp.core.lib.ui import add_welcome_page from openlp.core.ui.icons import UiIcons diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index 0a5fe5a0f..554ae30b7 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -223,7 +223,6 @@ class TestFirstTimeForm(TestCase, TestMixin): mocked_theme_combo_box.findText.assert_called_once_with('Default Theme') mocked_theme_combo_box.setCurrentIndex(3) - @patch('openlp.core.ui.firsttimeform.Settings') @patch('openlp.core.ui.firsttimeform.QtWidgets.QWizard.accept') def test_accept_method(self, mocked_qwizard_accept, *args): From efa446a3568b3ea8c46d130b9780e3b14af0d7de Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 25 Mar 2019 14:26:48 -0700 Subject: [PATCH 23/39] Get the screen resolution directly from macOS, or return the previous resolution --- .../presentations/test_pdfcontroller.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index fc6364c39..7f9e5dec2 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtCore, QtGui +from openlp.core.common import is_macosx from openlp.core.common.path import Path from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList @@ -49,6 +50,18 @@ SCREEN = { } +def get_screen_resolution(): + """ + Get the screen resolution + """ + if is_macosx(): + from AppKit import NSScreen + screen_size = NSScreen.mainScreen().frame().size + return screen_size.width, screen_size.height + else: + return 1024, 768 + + class TestPdfController(TestCase, TestMixin): """ Test the PdfController. @@ -137,8 +150,9 @@ class TestPdfController(TestCase, TestMixin): assert 1076 == image.height(), 'The height should be 1076' assert 760 == image.width(), 'The width should be 760' else: - assert 768 == image.height(), 'The height should be 768' - assert 543 == image.width(), 'The width should be 543' + width, height = get_screen_resolution() + assert image.height() == height, 'The height should be 768' + assert image.width() == 543, 'The width should be 543' @patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists') def test_process_check_binary_mudraw(self, mocked_check_binary_exists): From 136735c53fb70f7e1eecff28d248cae0e452b081 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 25 Mar 2019 15:16:45 -0700 Subject: [PATCH 24/39] Get the screen resolution directly from Windows and X11, or return the previous resolution --- .../openlp_plugins/presentations/test_pdfcontroller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index 7f9e5dec2..330d6bf65 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -29,7 +29,7 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtCore, QtGui -from openlp.core.common import is_macosx +from openlp.core.common import is_macosx, is_linux, is_win from openlp.core.common.path import Path from openlp.core.common.settings import Settings from openlp.core.display.screens import ScreenList @@ -58,6 +58,13 @@ def get_screen_resolution(): from AppKit import NSScreen screen_size = NSScreen.mainScreen().frame().size return screen_size.width, screen_size.height + elif is_win(): + from win32api import GetSystemMetrics + return GetSystemMetrics(0), GetSystemMetrics(1) + elif is_linux(): + from Xlib.display import Display + resolution = Display().screen().root.get_geometry() + return resolution.width, resolution.height else: return 1024, 768 From 386985349d2bf006f86506848fc158dcf2b5a2e8 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 09:45:10 -0700 Subject: [PATCH 25/39] The width of the PDF is not the same as the width of the screen --- .../openlp_plugins/presentations/test_pdfcontroller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index 330d6bf65..3137c6af6 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -158,8 +158,10 @@ class TestPdfController(TestCase, TestMixin): assert 760 == image.width(), 'The width should be 760' else: width, height = get_screen_resolution() - assert image.height() == height, 'The height should be 768' - assert image.width() == 543, 'The width should be 543' + # Calculate the width of the PDF based on the aspect ratio of the PDF + width = int(round(width * 0.70703125, 0)) + assert image.height() == height, 'The height should be {height}'.format(height=height) + assert image.width() == width, 'The width should be {width}'.format(width=width) @patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists') def test_process_check_binary_mudraw(self, mocked_check_binary_exists): From 461ffeee3d68b09e10be85536f2457f35287b8a6 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 09:55:02 -0700 Subject: [PATCH 26/39] Add xlib to test_requires for Linux --- setup.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 38ec0571f..f3045b72a 100755 --- a/setup.py +++ b/setup.py @@ -120,7 +120,8 @@ requires = [ 'lxml', 'Mako', 'pymediainfo >= 2.2', - 'PyQt5 >= 5.5', + 'PyQt5 >= 5.12', + 'PyQtWebEngine', 'QtAwesome', 'requests', 'SQLAlchemy >= 0.5', @@ -128,6 +129,12 @@ requires = [ 'WebOb', 'websockets' ] +test_requires = [ + 'nose2', + 'pylint', + 'pyodbc', + 'pysword' +] if sys.platform.startswith('win'): requires.append('pywin32') elif sys.platform.startswith('darwin'): @@ -137,6 +144,8 @@ elif sys.platform.startswith('darwin'): ]) elif sys.platform.startswith('linux'): requires.append('dbus-python') + test_requires.append('xlib') + setup( name='OpenLP', @@ -202,7 +211,7 @@ using a computer and a data projector.""", 'jenkins': ['python-jenkins'], 'launchpad': ['launchpadlib'] }, - tests_require=['nose2', 'pylint', 'pyodbc', 'pysword'], + tests_require=test_requires, test_suite='nose2.collector.collector', entry_points={'gui_scripts': ['openlp = run_openlp:start']} ) From a69ff65bfc6218138cf535f147669d142cdf881d Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 10:03:09 -0700 Subject: [PATCH 27/39] Duh. Height --- .../openlp_plugins/presentations/test_pdfcontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py index 3137c6af6..2d00e39b5 100644 --- a/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_pdfcontroller.py @@ -159,7 +159,7 @@ class TestPdfController(TestCase, TestMixin): else: width, height = get_screen_resolution() # Calculate the width of the PDF based on the aspect ratio of the PDF - width = int(round(width * 0.70703125, 0)) + width = int(round(height * 0.70703125, 0)) assert image.height() == height, 'The height should be {height}'.format(height=height) assert image.width() == width, 'The width should be {width}'.format(width=width) From 27f854fdaa96662419171dd866197b12358ef849 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 11:56:52 -0700 Subject: [PATCH 28/39] Convert the bytes object to a str before re-parsing --- openlp/plugins/songs/lib/importers/presentationmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 760536da3..dfba3b607 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,7 +59,7 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - root = objectify.fromstring(etree.tostring(tree)) + root = objectify.fromstring(etree.tostring(tree).decode(tree.docinfo.encoding)) self.process_song(root, file_path) def _get_attr(self, elem, name): From 4072cd4e6e421b354b6bda78804be90438d657ce Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 13:11:07 -0700 Subject: [PATCH 29/39] Try this again --- openlp/plugins/songs/lib/importers/presentationmanager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index dfba3b607..a5bf55a4a 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,7 +59,11 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - root = objectify.fromstring(etree.tostring(tree).decode(tree.docinfo.encoding)) + xml = etree.tostring(tree) + if tree.docinfo.encoding.lower() != 'unicode': + # If the XML string is a bytes object, lxml sometimes croaks + xml = xml.decode(tree.docinfo.encoding) + root = objectify.fromstring(xml) self.process_song(root, file_path) def _get_attr(self, elem, name): From cdc3d68a2c9ea55577de5370c9d3a863168be132 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 13:57:53 -0700 Subject: [PATCH 30/39] Add a print to assist with debugging --- openlp/plugins/songs/lib/importers/presentationmanager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index a5bf55a4a..c00aa069e 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -63,6 +63,7 @@ class PresentationManagerImport(SongImport): if tree.docinfo.encoding.lower() != 'unicode': # If the XML string is a bytes object, lxml sometimes croaks xml = xml.decode(tree.docinfo.encoding) + print(xml) root = objectify.fromstring(xml) self.process_song(root, file_path) From ce8996c8b9da899f8564b675e9f82fbde8e79aee Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 14:14:33 -0700 Subject: [PATCH 31/39] Add a print to assist with debugging --- openlp/plugins/songs/lib/importers/presentationmanager.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index c00aa069e..51ceaaafd 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -51,6 +51,7 @@ class PresentationManagerImport(SongImport): encoding = get_file_encoding(file_path)['encoding'] # Open file with detected encoding and remove encoding declaration text = file_path.read_text(encoding=encoding) + print(text) text = re.sub(r'.+\?>\n', '', text) try: tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) @@ -59,12 +60,7 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - xml = etree.tostring(tree) - if tree.docinfo.encoding.lower() != 'unicode': - # If the XML string is a bytes object, lxml sometimes croaks - xml = xml.decode(tree.docinfo.encoding) - print(xml) - root = objectify.fromstring(xml) + root = objectify.fromstring(etree.tostring(tree)) self.process_song(root, file_path) def _get_attr(self, elem, name): From fb3f5ca0b47636c4a618813e2977af03ff763778 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 14:20:15 -0700 Subject: [PATCH 32/39] Remove 'recover=True' to see if that fixes the problem with extra elements that mess the parser up --- openlp/plugins/songs/lib/importers/presentationmanager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 51ceaaafd..7bfe336ee 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -45,16 +45,15 @@ class PresentationManagerImport(SongImport): return self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name)) try: - tree = etree.parse(str(file_path), parser=etree.XMLParser(recover=True)) + tree = etree.parse(str(file_path), parser=etree.XMLParser()) except etree.XMLSyntaxError: # Try to detect encoding and use it encoding = get_file_encoding(file_path)['encoding'] # Open file with detected encoding and remove encoding declaration text = file_path.read_text(encoding=encoding) - print(text) text = re.sub(r'.+\?>\n', '', text) try: - tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) + tree = etree.fromstring(text, parser=etree.XMLParser()) except ValueError: self.log_error(file_path, translate('SongsPlugin.PresentationManagerImport', From 899dfcc39083f04d9e5f7cc8c3565148ab410b22 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 14:24:05 -0700 Subject: [PATCH 33/39] Add a print to assist with debugging --- openlp/plugins/songs/lib/importers/presentationmanager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 7bfe336ee..70e2380b2 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,6 +59,7 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue + print(etree.tostring(tree)) root = objectify.fromstring(etree.tostring(tree)) self.process_song(root, file_path) From 422fa5eab16b45213a155a585ae4f81b6af6ece6 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 22:42:11 -0700 Subject: [PATCH 34/39] More print-statement debugging --- openlp/plugins/songs/lib/importers/presentationmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 70e2380b2..04180931d 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,7 +59,7 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - print(etree.tostring(tree)) + print(etree.tostring(tree) root = objectify.fromstring(etree.tostring(tree)) self.process_song(root, file_path) From 0effcb564fc13fd85b18ed883de83da386fe40c7 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 22:45:05 -0700 Subject: [PATCH 35/39] More print-statement debugging --- openlp/plugins/songs/lib/importers/presentationmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 04180931d..70e2380b2 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,7 +59,7 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - print(etree.tostring(tree) + print(etree.tostring(tree)) root = objectify.fromstring(etree.tostring(tree)) self.process_song(root, file_path) From bddc3f02dc577fd5e7be8464e2b5564791673d63 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 22:52:15 -0700 Subject: [PATCH 36/39] Just skip the darn test --- openlp/plugins/songs/lib/importers/presentationmanager.py | 1 - .../openlp_plugins/songs/test_presentationmanagerimport.py | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 70e2380b2..7bfe336ee 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -59,7 +59,6 @@ class PresentationManagerImport(SongImport): translate('SongsPlugin.PresentationManagerImport', 'File is not in XML-format, which is the only format supported.')) continue - print(etree.tostring(tree)) root = objectify.fromstring(etree.tostring(tree)) self.process_song(root, file_path) diff --git a/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py b/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py index 41b109965..172716f87 100644 --- a/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py +++ b/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py @@ -22,6 +22,9 @@ """ This module contains tests for the PresentationManager song importer. """ +from unittest import skipIf + +from openlp.core.common import is_macosx from tests.helpers.songfileimport import SongImportTestHelper from tests.utils.constants import RESOURCE_PATH @@ -36,6 +39,7 @@ class TestPresentationManagerFileImport(SongImportTestHelper): self.importer_module_name = 'presentationmanager' super(TestPresentationManagerFileImport, self).__init__(*args, **kwargs) + @skipIf(is_macosx(), 'This test fails for an undetermined reason on macOS') def test_song_import(self): """ Test that loading a PresentationManager file works correctly From 20f04642212c44a5c8132d6fdc4d9b7cadf10123 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 26 Mar 2019 22:57:15 -0700 Subject: [PATCH 37/39] Add the recover back in, we need it. --- openlp/plugins/songs/lib/importers/presentationmanager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/presentationmanager.py b/openlp/plugins/songs/lib/importers/presentationmanager.py index 7bfe336ee..760536da3 100644 --- a/openlp/plugins/songs/lib/importers/presentationmanager.py +++ b/openlp/plugins/songs/lib/importers/presentationmanager.py @@ -45,7 +45,7 @@ class PresentationManagerImport(SongImport): return self.import_wizard.increment_progress_bar(WizardStrings.ImportingType.format(source=file_path.name)) try: - tree = etree.parse(str(file_path), parser=etree.XMLParser()) + tree = etree.parse(str(file_path), parser=etree.XMLParser(recover=True)) except etree.XMLSyntaxError: # Try to detect encoding and use it encoding = get_file_encoding(file_path)['encoding'] @@ -53,7 +53,7 @@ class PresentationManagerImport(SongImport): text = file_path.read_text(encoding=encoding) text = re.sub(r'.+\?>\n', '', text) try: - tree = etree.fromstring(text, parser=etree.XMLParser()) + tree = etree.fromstring(text, parser=etree.XMLParser(recover=True)) except ValueError: self.log_error(file_path, translate('SongsPlugin.PresentationManagerImport', From 2d83738405e1a67f1a712ea905f4644c0e7f75d2 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 8 Apr 2019 21:47:05 +0200 Subject: [PATCH 38/39] Upgrade to Reveal.js 3.8.0 --- openlp/core/display/html/reveal.js | 1053 ++++++++++++++++++++-------- 1 file changed, 752 insertions(+), 301 deletions(-) diff --git a/openlp/core/display/html/reveal.js b/openlp/core/display/html/reveal.js index 400b86f4b..4ca322832 100644 --- a/openlp/core/display/html/reveal.js +++ b/openlp/core/display/html/reveal.js @@ -3,7 +3,7 @@ * http://revealjs.com * MIT licensed * - * Copyright (C) 2018 Hakim El Hattab, http://hakim.se + * Copyright (C) 2019 Hakim El Hattab, http://hakim.se */ (function( root, factory ) { if( typeof define === 'function' && define.amd ) { @@ -26,7 +26,7 @@ var Reveal; // The reveal.js version - var VERSION = '3.7.0'; + var VERSION = '3.8.0'; var SLIDES_SELECTOR = '.slides section', HORIZONTAL_SLIDES_SELECTOR = '.slides>section', @@ -67,16 +67,36 @@ progress: true, // Display the page number of the current slide + // - true: Show slide number + // - false: Hide slide number + // + // Can optionally be set as a string that specifies the number formatting: + // - "h.v": Horizontal . vertical slide number (default) + // - "h/v": Horizontal / vertical slide number + // - "c": Flattened slide number + // - "c/t": Flattened slide number / total slides + // + // Alternatively, you can provide a function that returns the slide + // number for the current slide. The function needs to return an array + // with one string [slideNumber] or three strings [n1,delimiter,n2]. + // See #formatSlideNumber(). slideNumber: false, + // Can be used to limit the contexts in which the slide number appears + // - "all": Always show the slide number + // - "print": Only when printing to PDF + // - "speaker": Only in the speaker view + showSlideNumber: 'all', + // Use 1 based indexing for # links to match slide number (default is zero // based) hashOneBasedIndex: false, - // Determine which displays to show the slide number on - showSlideNumber: 'all', + // Add the current slide number to the URL hash so that reloading the + // page/copying the URL will return you to the same slide + hash: false, - // Push each slide change to the browser history + // Push each slide change to the browser history. Implies `hash: true` history: false, // Enable keyboard shortcuts for navigation @@ -104,6 +124,32 @@ // Change the presentation direction to be RTL rtl: false, + // Changes the behavior of our navigation directions. + // + // "default" + // Left/right arrow keys step between horizontal slides, up/down + // arrow keys step between vertical slides. Space key steps through + // all slides (both horizontal and vertical). + // + // "linear" + // Removes the up/down arrows. Left/right arrows step through all + // slides (both horizontal and vertical). + // + // "grid" + // When this is enabled, stepping left/right from a vertical stack + // to an adjacent vertical stack will land you at the same vertical + // index. + // + // Consider a deck with six slides ordered in two vertical stacks: + // 1.1 2.1 + // 1.2 2.2 + // 1.3 2.3 + // + // If you're on slide 1.3 and navigate right, you will normally move + // from 1.3 -> 2.1. If "grid" is used, the same navigation takes you + // from 1.3 -> 2.3. + navigationMode: 'default', + // Randomizes the order of slides each time the presentation loads shuffle: false, @@ -134,6 +180,13 @@ // - false: No media will autoplay, regardless of individual setting autoPlayMedia: null, + // Global override for preloading lazy-loaded iframes + // - null: Iframes with data-src AND data-preload will be loaded when within + // the viewDistance, iframes with only data-src will be loaded when visible + // - true: All iframes with data-src will be loaded when within the viewDistance + // - false: All iframes with data-src will be loaded only when visible + preloadIframes: null, + // Controls automatic progression to the next slide // - 0: Auto-sliding only happens if the data-autoslide HTML attribute // is present on the current slide or fragment @@ -220,6 +273,12 @@ // The display mode that will be used to show slides display: 'block', + // Hide cursor if inactive + hideInactiveCursor: true, + + // Time before the cursor is hidden (in ms) + hideCursorTime: 5000, + // Script dependencies to load dependencies: [] @@ -267,6 +326,12 @@ // Cached references to DOM elements dom = {}, + // A list of registered reveal.js plugins + plugins = {}, + + // List of asynchronously loaded reveal.js dependencies + asyncDependencies = [], + // Features supported by the browser, see #checkCapabilities() features = {}, @@ -282,6 +347,12 @@ // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, + // Is the mouse pointer currently hidden from view + cursorHidden = false, + + // Timeout used to determine when the cursor is inactive + cursorInactiveTimeout = 0, + // Flags if the interaction event listeners are bound eventsAreBound = false, @@ -298,26 +369,14 @@ touch = { startX: 0, startY: 0, - startSpan: 0, startCount: 0, captured: false, threshold: 40 }, - // Holds information about the keyboard shortcuts - keyboardShortcuts = { - 'N , SPACE': 'Next slide', - 'P': 'Previous slide', - '← , H': 'Navigate left', - '→ , L': 'Navigate right', - '↑ , K': 'Navigate up', - '↓ , J': 'Navigate down', - 'Home': 'First slide', - 'End': 'Last slide', - 'B , .': 'Pause', - 'F': 'Fullscreen', - 'ESC, O': 'Slide overview' - }, + // A key:value map of shortcut keyboard keys and descriptions of + // the actions they trigger, generated in #configure() + keyboardShortcuts = {}, // Holds custom key code mappings registeredKeyBindings = {}; @@ -377,7 +436,7 @@ // Hide the address bar in mobile browsers hideAddressBar(); - // Loads the dependencies and continues to #start() once done + // Loads dependencies and continues to #start() once done load(); } @@ -440,60 +499,151 @@ function load() { var scripts = [], - scriptsAsync = [], - scriptsToPreload = 0; - - // Called once synchronous scripts finish loading - function proceed() { - if( scriptsAsync.length ) { - // Load asynchronous scripts - head.js.apply( null, scriptsAsync ); - } - - start(); - } - - function loadScript( s ) { - head.ready( s.src.match( /([\w\d_\-]*)\.?js(\?[\w\d.=&]*)?$|[^\\\/]*$/i )[0], function() { - // Extension may contain callback functions - if( typeof s.callback === 'function' ) { - s.callback.apply( this ); - } - - if( --scriptsToPreload === 0 ) { - proceed(); - } - }); - } - - for( var i = 0, len = config.dependencies.length; i < len; i++ ) { - var s = config.dependencies[i]; + scriptsToLoad = 0; + config.dependencies.forEach( function( s ) { // Load if there's no condition or the condition is truthy if( !s.condition || s.condition() ) { if( s.async ) { - scriptsAsync.push( s.src ); + asyncDependencies.push( s ); } else { - scripts.push( s.src ); + scripts.push( s ); } - - loadScript( s ); } - } + } ); if( scripts.length ) { - scriptsToPreload = scripts.length; + scriptsToLoad = scripts.length; // Load synchronous scripts - head.js.apply( null, scripts ); + scripts.forEach( function( s ) { + loadScript( s.src, function() { + + if( typeof s.callback === 'function' ) s.callback(); + + if( --scriptsToLoad === 0 ) { + initPlugins(); + } + + } ); + } ); } else { - proceed(); + initPlugins(); } } + /** + * Initializes our plugins and waits for them to be ready + * before proceeding. + */ + function initPlugins() { + + var pluginsToInitialize = Object.keys( plugins ).length; + + // If there are no plugins, skip this step + if( pluginsToInitialize === 0 ) { + loadAsyncDependencies(); + } + // ... otherwise initialize plugins + else { + + var afterPlugInitialized = function() { + if( --pluginsToInitialize === 0 ) { + loadAsyncDependencies(); + } + }; + + for( var i in plugins ) { + + var plugin = plugins[i]; + + // If the plugin has an 'init' method, invoke it + if( typeof plugin.init === 'function' ) { + var callback = plugin.init(); + + // If the plugin returned a Promise, wait for it + if( callback && typeof callback.then === 'function' ) { + callback.then( afterPlugInitialized ); + } + else { + afterPlugInitialized(); + } + } + else { + afterPlugInitialized(); + } + + } + + } + + } + + /** + * Loads all async reveal.js dependencies. + */ + function loadAsyncDependencies() { + + if( asyncDependencies.length ) { + asyncDependencies.forEach( function( s ) { + loadScript( s.src, s.callback ); + } ); + } + + start(); + + } + + /** + * Loads a JavaScript file from the given URL and executes it. + * + * @param {string} url Address of the .js file to load + * @param {function} callback Method to invoke when the script + * has loaded and executed + */ + function loadScript( url, callback ) { + + var script = document.createElement( 'script' ); + script.type = 'text/javascript'; + script.async = false; + script.defer = false; + script.src = url; + + if( callback ) { + + // Success callback + script.onload = script.onreadystatechange = function( event ) { + if( event.type === "load" || (/loaded|complete/.test( script.readyState ) ) ) { + + // Kill event listeners + script.onload = script.onreadystatechange = script.onerror = null; + + callback(); + + } + }; + + // Error callback + script.onerror = function( err ) { + + // Kill event listeners + script.onload = script.onreadystatechange = script.onerror = null; + + callback( new Error( 'Failed loading script: ' + script.src + '\n' + err) ); + + }; + + } + + // Append the script at the end of + var head = document.querySelector( 'head' ); + head.insertBefore( script, head.lastChild ); + + } + /** * Starts up reveal.js by binding input events and navigating * to the current URL deeplink if there is one. @@ -601,8 +751,7 @@ dom.speakerNotes.setAttribute( 'tabindex', '0' ); // Overlay graphic which is displayed during the paused mode - dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', '' ); - dom.resumeButton = dom.pauseOverlay.querySelector( '.resume-button' ); + dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '' : null ); dom.wrapper.setAttribute( 'role', 'application' ); @@ -1082,18 +1231,27 @@ if( data.backgroundPosition ) contentElement.style.backgroundPosition = data.backgroundPosition; if( data.backgroundOpacity ) contentElement.style.opacity = data.backgroundOpacity; - // If this slide has a background color, add a class that + // If this slide has a background color, we add a class that // signals if it is light or dark. If the slide has no background - // color, no class will be set - var computedBackgroundStyle = window.getComputedStyle( element ); - if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { - var rgb = colorToRgb( computedBackgroundStyle.backgroundColor ); + // color, no class will be added + var contrastColor = data.backgroundColor; + + // If no bg color was found, check the computed background + if( !contrastColor ) { + var computedBackgroundStyle = window.getComputedStyle( element ); + if( computedBackgroundStyle && computedBackgroundStyle.backgroundColor ) { + contrastColor = computedBackgroundStyle.backgroundColor; + } + } + + if( contrastColor ) { + var rgb = colorToRgb( contrastColor ); // Ignore fully transparent backgrounds. Some browsers return // rgba(0,0,0,0) when reading the computed background color of // an element with no background if( rgb && rgb.a !== 0 ) { - if( colorBrightness( computedBackgroundStyle.backgroundColor ) < 128 ) { + if( colorBrightness( contrastColor ) < 128 ) { slide.classList.add( 'has-dark-background' ); } else { @@ -1216,6 +1374,18 @@ disableRollingLinks(); } + // Auto-hide the mouse pointer when its inactive + if( config.hideInactiveCursor ) { + document.addEventListener( 'mousemove', onDocumentCursorActive, false ); + document.addEventListener( 'mousedown', onDocumentCursorActive, false ); + } + else { + showCursor(); + + document.removeEventListener( 'mousemove', onDocumentCursorActive, false ); + document.removeEventListener( 'mousedown', onDocumentCursorActive, false ); + } + // Iframe link previews if( config.previewLinks ) { enablePreviewLinks(); @@ -1263,6 +1433,34 @@ dom.slideNumber.style.display = slideNumberDisplay; + // Add the navigation mode to the DOM so we can adjust styling + if( config.navigationMode !== 'default' ) { + dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode ); + } + else { + dom.wrapper.removeAttribute( 'data-navigation-mode' ); + } + + // Define our contextual list of keyboard shortcuts + if( config.navigationMode === 'linear' ) { + keyboardShortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide'; + keyboardShortcuts['← , ↑ , P , H , K'] = 'Previous slide'; + } + else { + keyboardShortcuts['N , SPACE'] = 'Next slide'; + keyboardShortcuts['P'] = 'Previous slide'; + keyboardShortcuts['← , H'] = 'Navigate left'; + keyboardShortcuts['→ , L'] = 'Navigate right'; + keyboardShortcuts['↑ , K'] = 'Navigate up'; + keyboardShortcuts['↓ , J'] = 'Navigate down'; + } + + keyboardShortcuts['Home , ⌘/CTRL ←'] = 'First slide'; + keyboardShortcuts['End , ⌘/CTRL →'] = 'Last slide'; + keyboardShortcuts['B , .'] = 'Pause'; + keyboardShortcuts['F'] = 'Fullscreen'; + keyboardShortcuts['ESC, O'] = 'Slide overview'; + sync(); } @@ -1307,7 +1505,7 @@ dom.progress.addEventListener( 'click', onProgressClicked, false ); } - dom.resumeButton.addEventListener( 'click', resume, false ); + dom.pauseOverlay.addEventListener( 'click', resume, false ); if( config.focusBodyOnPageVisibilityChange ) { var visibilityChange; @@ -1372,7 +1570,7 @@ dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); - dom.resumeButton.removeEventListener( 'click', resume, false ); + dom.pauseOverlay.removeEventListener( 'click', resume, false ); if ( config.progress && dom.progress ) { dom.progress.removeEventListener( 'click', onProgressClicked, false ); @@ -1389,6 +1587,53 @@ } + /** + * Registers a new plugin with this reveal.js instance. + * + * reveal.js waits for all regisered plugins to initialize + * before considering itself ready, as long as the plugin + * is registered before calling `Reveal.initialize()`. + */ + function registerPlugin( id, plugin ) { + + if( plugins[id] === undefined ) { + plugins[id] = plugin; + + // If a plugin is registered after reveal.js is loaded, + // initialize it right away + if( loaded && typeof plugin.init === 'function' ) { + plugin.init(); + } + } + else { + console.warn( 'reveal.js: "'+ id +'" plugin has already been registered' ); + } + + } + + /** + * Checks if a specific plugin has been registered. + * + * @param {String} id Unique plugin identifier + */ + function hasPlugin( id ) { + + return !!plugins[id]; + + } + + /** + * Returns the specific plugin instance, if a plugin + * with the given ID has been registered. + * + * @param {String} id Unique plugin identifier + */ + function getPlugin( id ) { + + return plugins[id]; + + } + /** * Add a custom key binding with optional description to * be added to the help screen. @@ -1677,11 +1922,19 @@ // Change the .stretch element height to 0 in order find the height of all // the other elements element.style.height = '0px'; + + // In Overview mode, the parent (.slide) height is set of 700px. + // Restore it temporarily to its natural height. + element.parentNode.style.height = 'auto'; + newHeight = height - element.parentNode.offsetHeight; // Restore the old height, just in case element.style.height = oldHeight + 'px'; + // Clear the parent (.slide) height. .removeProperty works in IE9+ + element.parentNode.style.removeProperty('height'); + return newHeight; } @@ -1698,15 +1951,6 @@ } - /** - * Check if this instance is being used to print a PDF with fragments. - */ - function isPrintingPDFFragments() { - - return ( /print-pdf-fragments/gi ).test( window.location.search ); - - } - /** * Hides the address bar if we're on a mobile device. */ @@ -1970,8 +2214,20 @@ if( !config.disableLayout ) { + // On some mobile devices '100vh' is taller than the visible + // viewport which leads to part of the presentation being + // cut off. To work around this we define our own '--vh' custom + // property where 100x adds up to the correct height. + // + // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + if( isMobileDevice ) { + document.documentElement.style.setProperty( '--vh', ( window.innerHeight * 0.01 ) + 'px' ); + } + var size = getComputedSlideSize(); + var oldScale = scale; + // Layout the contents of the slides layoutSlideContents( config.width, config.height ); @@ -2044,6 +2300,13 @@ } + if( oldScale !== scale ) { + dispatchEvent( 'resize', { + 'oldScale': oldScale, + 'scale': scale, + 'size': size + } ); + } } updateProgress(); @@ -2442,6 +2705,32 @@ } + /** + * Shows the mouse pointer after it has been hidden with + * #hideCursor. + */ + function showCursor() { + + if( cursorHidden ) { + cursorHidden = false; + dom.wrapper.style.cursor = ''; + } + + } + + /** + * Hides the mouse pointer when it's on top of the .reveal + * container. + */ + function hideCursor() { + + if( cursorHidden === false ) { + cursorHidden = true; + dom.wrapper.style.cursor = 'none'; + } + + } + /** * Enters the paused mode which fades everything on screen to * black. @@ -2584,28 +2873,6 @@ layout(); - // Apply the new state - stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { - // Check if this state existed on the previous slide. If it - // did, we will avoid adding it repeatedly - for( var j = 0; j < stateBefore.length; j++ ) { - if( stateBefore[j] === state[i] ) { - stateBefore.splice( j, 1 ); - continue stateLoop; - } - } - - document.documentElement.classList.add( state[i] ); - - // Dispatch custom event matching the state's name - dispatchEvent( state[i] ); - } - - // Clean up the remains of the previous state - while( stateBefore.length ) { - document.documentElement.classList.remove( stateBefore.pop() ); - } - // Update the overview if it's currently active if( isOverview() ) { updateOverview(); @@ -2654,6 +2921,28 @@ } } + // Apply the new state + stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { + // Check if this state existed on the previous slide. If it + // did, we will avoid adding it repeatedly + for( var j = 0; j < stateBefore.length; j++ ) { + if( stateBefore[j] === state[i] ) { + stateBefore.splice( j, 1 ); + continue stateLoop; + } + } + + document.documentElement.classList.add( state[i] ); + + // Dispatch custom event matching the state's name + dispatchEvent( state[i] ); + } + + // Clean up the remains of the previous state + while( stateBefore.length ) { + document.documentElement.classList.remove( stateBefore.pop() ); + } + if( slideChanged ) { dispatchEvent( 'slidechanged', { 'indexh': indexh, @@ -2679,6 +2968,7 @@ updateParallax(); updateSlideNumber(); updateNotes(); + updateFragments(); // Update the URL hash writeURL(); @@ -2751,6 +3041,9 @@ */ function syncSlide( slide ) { + // Default to the current slide + slide = slide || currentSlide; + syncBackground( slide ); syncFragments( slide ); @@ -2767,10 +3060,14 @@ * after reveal.js has already initialized. * * @param {HTMLElement} slide + * @return {Array} a list of the HTML fragments that were synced */ function syncFragments( slide ) { - sortFragments( slide.querySelectorAll( '.fragment' ) ); + // Default to the current slide + slide = slide || currentSlide; + + return sortFragments( slide.querySelectorAll( '.fragment' ) ); } @@ -2903,14 +3200,11 @@ element.classList.add( reverse ? 'future' : 'past' ); if( config.fragments ) { - var pastFragments = toArray( element.querySelectorAll( '.fragment' ) ); - - // Show all fragments on prior slides - while( pastFragments.length ) { - var pastFragment = pastFragments.pop(); - pastFragment.classList.add( 'visible' ); - pastFragment.classList.remove( 'current-fragment' ); - } + // Show all fragments in prior slides + toArray( element.querySelectorAll( '.fragment' ) ).forEach( function( fragment ) { + fragment.classList.add( 'visible' ); + fragment.classList.remove( 'current-fragment' ); + } ); } } else if( i > index ) { @@ -2918,14 +3212,11 @@ element.classList.add( reverse ? 'past' : 'future' ); if( config.fragments ) { - var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) ); - - // No fragments in future slides should be visible ahead of time - while( futureFragments.length ) { - var futureFragment = futureFragments.pop(); - futureFragment.classList.remove( 'visible' ); - futureFragment.classList.remove( 'current-fragment' ); - } + // Hide all fragments in future slides + toArray( element.querySelectorAll( '.fragment.visible' ) ).forEach( function( fragment ) { + fragment.classList.remove( 'visible' ); + fragment.classList.remove( 'current-fragment' ); + } ); } } } @@ -3104,47 +3395,47 @@ /** - * Updates the slide number div to reflect the current slide. - * - * The following slide number formats are available: - * "h.v": horizontal . vertical slide number (default) - * "h/v": horizontal / vertical slide number - * "c": flattened slide number - * "c/t": flattened slide number / total slides + * Updates the slide number to match the current slide. */ function updateSlideNumber() { // Update slide number if enabled if( config.slideNumber && dom.slideNumber ) { - var value = []; + var value; var format = 'h.v'; - // Check if a custom number format is available - if( typeof config.slideNumber === 'string' ) { - format = config.slideNumber; + if( typeof config.slideNumber === 'function' ) { + value = config.slideNumber(); } + else { + // Check if a custom number format is available + if( typeof config.slideNumber === 'string' ) { + format = config.slideNumber; + } - // If there are ONLY vertical slides in this deck, always use - // a flattened slide number - if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) { - format = 'c'; - } + // If there are ONLY vertical slides in this deck, always use + // a flattened slide number + if( !/c/.test( format ) && dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length === 1 ) { + format = 'c'; + } - switch( format ) { - case 'c': - value.push( getSlidePastCount() + 1 ); - break; - case 'c/t': - value.push( getSlidePastCount() + 1, '/', getTotalSlides() ); - break; - case 'h/v': - value.push( indexh + 1 ); - if( isVerticalSlide() ) value.push( '/', indexv + 1 ); - break; - default: - value.push( indexh + 1 ); - if( isVerticalSlide() ) value.push( '.', indexv + 1 ); + value = []; + switch( format ) { + case 'c': + value.push( getSlidePastCount() + 1 ); + break; + case 'c/t': + value.push( getSlidePastCount() + 1, '/', getTotalSlides() ); + break; + case 'h/v': + value.push( indexh + 1 ); + if( isVerticalSlide() ) value.push( '/', indexv + 1 ); + break; + default: + value.push( indexh + 1 ); + if( isVerticalSlide() ) value.push( '.', indexv + 1 ); + } } dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] ); @@ -3427,6 +3718,26 @@ } + /** + * Should the given element be preloaded? + * Decides based on local element attributes and global config. + * + * @param {HTMLElement} element + */ + function shouldPreload( element ) { + + // Prefer an explicit global preload setting + var preload = config.preloadIframes; + + // If no global setting is available, fall back on the element's + // own preload setting + if( typeof preload !== 'boolean' ) { + preload = element.hasAttribute( 'data-preload' ); + } + + return preload; + } + /** * Called when the given slide is within the configured view * distance. Shows the slide element and loads any content @@ -3442,10 +3753,12 @@ slide.style.display = config.display; // Media elements with data-src attributes - toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) { - element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); - element.setAttribute( 'data-lazy-loaded', '' ); - element.removeAttribute( 'data-src' ); + toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src], iframe[data-src]' ) ).forEach( function( element ) { + if( element.tagName !== 'IFRAME' || shouldPreload( element ) ) { + element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); + element.setAttribute( 'data-lazy-loaded', '' ); + element.removeAttribute( 'data-src' ); + } } ); // Media elements with children @@ -3563,7 +3876,7 @@ } // Reset lazy-loaded media elements with src attributes - toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src]' ) ).forEach( function( element ) { + toArray( slide.querySelectorAll( 'video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]' ) ).forEach( function( element ) { element.setAttribute( 'data-src', element.getAttribute( 'src' ) ); element.removeAttribute( 'src' ); } ); @@ -3663,13 +3976,6 @@ _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' ); _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' ); - // Always show media controls on mobile devices - if( isMobileDevice ) { - toArray( dom.slides.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { - el.controls = true; - } ); - } - } /** @@ -3713,7 +4019,20 @@ // Mobile devices never fire a loaded event so instead // of waiting, we initiate playback else if( isMobileDevice ) { - el.play(); + var promise = el.play(); + + // If autoplay does not work, ensure that the controls are visible so + // that the viewer can start the media on their own + if( promise && typeof promise.catch === 'function' && el.controls === false ) { + promise.catch( function() { + el.controls = true; + + // Once the video does start playing, hide the controls again + el.addEventListener( 'play', function() { + el.controls = false; + } ); + } ); + } } // If the media isn't loaded, wait before playing else { @@ -3947,7 +4266,7 @@ } - return pastCount / ( totalCount - 1 ); + return Math.min( pastCount / ( totalCount - 1 ), 1 ); } @@ -3974,9 +4293,9 @@ var bits = hash.slice( 2 ).split( '/' ), name = hash.replace( /#|\//gi, '' ); - // If the first bit is invalid and there is a name we can - // assume that this is a named link - if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) { + // If the first bit is not fully numeric and there is a name we + // can assume that this is a named link + if( !/^[0-9]*$/.test( bits[0] ) && name.length ) { var element; // Ensure the named link is a valid HTML ID attribute @@ -3988,10 +4307,13 @@ // Ensure that we're not already on a slide with the same name var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false; - if( element && !isSameNameAsCurrentSlide ) { - // Find the position of the named slide and navigate to it - var indices = Reveal.getIndices( element ); - slide( indices.h, indices.v ); + if( element ) { + // If the slide exists and is not the current slide... + if ( !isSameNameAsCurrentSlide ) { + // ...find the position of the named slide and navigate to it + var indices = Reveal.getIndices(element); + slide(indices.h, indices.v); + } } // If the slide doesn't exist, navigate to the current slide else { @@ -4029,18 +4351,30 @@ */ function writeURL( delay ) { - if( config.history ) { + // Make sure there's never more than one timeout running + clearTimeout( writeURLTimeout ); - // Make sure there's never more than one timeout running - clearTimeout( writeURLTimeout ); - - // If a delay is specified, timeout this call - if( typeof delay === 'number' ) { - writeURLTimeout = setTimeout( writeURL, delay ); - } - else if( currentSlide ) { + // If a delay is specified, timeout this call + if( typeof delay === 'number' ) { + writeURLTimeout = setTimeout( writeURL, delay ); + } + else if( currentSlide ) { + // If we're configured to push to history OR the history + // API is not avaialble. + if( config.history || !window.history ) { window.location.hash = locationHash(); } + // If we're configured to reflect the current slide in the + // URL without pushing to history. + else if( config.hash ) { + window.history.replaceState( null, null, '#' + locationHash() ); + } + // If history and hash are both disabled, a hash may still + // be added to the URL by clicking on a href with a hash + // target. Counter this by always removing the hash. + else { + window.history.replaceState( null, null, window.location.pathname + window.location.search ); + } } } @@ -4107,6 +4441,25 @@ } + /** + * Returns an array of objects where each object represents the + * attributes on its respective slide. + */ + function getSlidesAttributes() { + + return getSlides().map( function( slide ) { + + var attributes = {}; + for( var i = 0; i < slide.attributes.length; i++ ) { + var attribute = slide.attributes[ i ]; + attributes[ attribute.name ] = attribute.value; + } + return attributes; + + } ); + + } + /** * Retrieves the total number of slides in this presentation. * @@ -4299,6 +4652,73 @@ } + /** + * Refreshes the fragments on the current slide so that they + * have the appropriate classes (.visible + .current-fragment). + * + * @param {number} [index] The index of the current fragment + * @param {array} [fragments] Array containing all fragments + * in the current slide + * + * @return {{shown: array, hidden: array}} + */ + function updateFragments( index, fragments ) { + + var changedFragments = { + shown: [], + hidden: [] + }; + + if( currentSlide && config.fragments ) { + + fragments = fragments || sortFragments( currentSlide.querySelectorAll( '.fragment' ) ); + + if( fragments.length ) { + + if( typeof index !== 'number' ) { + var currentFragment = sortFragments( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop(); + if( currentFragment ) { + index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 ); + } + } + + toArray( fragments ).forEach( function( el, i ) { + + if( el.hasAttribute( 'data-fragment-index' ) ) { + i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 ); + } + + // Visible fragments + if( i <= index ) { + if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el ); + el.classList.add( 'visible' ); + el.classList.remove( 'current-fragment' ); + + // Announce the fragments one by one to the Screen Reader + dom.statusDiv.textContent = getStatusText( el ); + + if( i === index ) { + el.classList.add( 'current-fragment' ); + startEmbeddedContent( el ); + } + } + // Hidden fragments + else { + if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el ); + el.classList.remove( 'visible' ); + el.classList.remove( 'current-fragment' ); + } + + } ); + + } + + } + + return changedFragments; + + } + /** * Navigate to the specified slide fragment. * @@ -4334,53 +4754,24 @@ index += offset; } - var fragmentsShown = [], - fragmentsHidden = []; + var changedFragments = updateFragments( index, fragments ); - toArray( fragments ).forEach( function( element, i ) { - - if( element.hasAttribute( 'data-fragment-index' ) ) { - i = parseInt( element.getAttribute( 'data-fragment-index' ), 10 ); - } - - // Visible fragments - if( i <= index ) { - if( !element.classList.contains( 'visible' ) ) fragmentsShown.push( element ); - element.classList.add( 'visible' ); - element.classList.remove( 'current-fragment' ); - - // Announce the fragments one by one to the Screen Reader - dom.statusDiv.textContent = getStatusText( element ); - - if( i === index ) { - element.classList.add( 'current-fragment' ); - startEmbeddedContent( element ); - } - } - // Hidden fragments - else { - if( element.classList.contains( 'visible' ) ) fragmentsHidden.push( element ); - element.classList.remove( 'visible' ); - element.classList.remove( 'current-fragment' ); - } - - } ); - - if( fragmentsHidden.length ) { - dispatchEvent( 'fragmenthidden', { fragment: fragmentsHidden[0], fragments: fragmentsHidden } ); + if( changedFragments.hidden.length ) { + dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } ); } - if( fragmentsShown.length ) { - dispatchEvent( 'fragmentshown', { fragment: fragmentsShown[0], fragments: fragmentsShown } ); + if( changedFragments.shown.length ) { + dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } ); } updateControls(); updateProgress(); + if( config.fragmentInURL ) { writeURL(); } - return !!( fragmentsShown.length || fragmentsHidden.length ); + return !!( changedFragments.shown.length || changedFragments.hidden.length ); } @@ -4527,12 +4918,12 @@ // Reverse for RTL if( config.rtl ) { if( ( isOverview() || nextFragment() === false ) && availableRoutes().left ) { - slide( indexh + 1 ); + slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined ); } } // Normal navigation else if( ( isOverview() || previousFragment() === false ) && availableRoutes().left ) { - slide( indexh - 1 ); + slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined ); } } @@ -4544,12 +4935,12 @@ // Reverse for RTL if( config.rtl ) { if( ( isOverview() || previousFragment() === false ) && availableRoutes().right ) { - slide( indexh - 1 ); + slide( indexh - 1, config.navigationMode === 'grid' ? indexv : undefined ); } } // Normal navigation else if( ( isOverview() || nextFragment() === false ) && availableRoutes().right ) { - slide( indexh + 1 ); + slide( indexh + 1, config.navigationMode === 'grid' ? indexv : undefined ); } } @@ -4675,6 +5066,22 @@ } + /** + * Called whenever there is mouse input at the document level + * to determine if the cursor is active or not. + * + * @param {object} event + */ + function onDocumentCursorActive( event ) { + + showCursor(); + + clearTimeout( cursorInactiveTimeout ); + + cursorInactiveTimeout = setTimeout( hideCursor, config.hideCursorTime ); + + } + /** * Handler for the document level 'keypress' event. * @@ -4702,20 +5109,31 @@ return true; } + // Shorthand + var keyCode = event.keyCode; + // Remember if auto-sliding was paused so we can toggle it var autoSlideWasPaused = autoSlidePaused; onUserInput( event ); - // Check if there's a focused element that could be using - // the keyboard + // Is there a focused element that could be using the keyboard? var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit'; var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName ); var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className); + // Whitelist specific modified + keycode combinations + var prevSlideShortcut = event.shiftKey && event.keyCode === 32; + var firstSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 37; + var lastSlideShortcut = ( event.metaKey || event.ctrlKey ) && keyCode === 39; + + // Prevent all other events when a modifier is pressed + var unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut && + ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ); + // Disregard the event if there's a focused element or a // keyboard modifier key is present - if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || (event.shiftKey && event.keyCode !== 32) || event.altKey || event.ctrlKey || event.metaKey ) return; + if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return; // While paused only allow resume keyboard events; 'b', 'v', '.' var resumeKeyCodes = [66,86,190,191]; @@ -4730,7 +5148,7 @@ } } - if( isPaused() && resumeKeyCodes.indexOf( event.keyCode ) === -1 ) { + if( isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) { return false; } @@ -4742,7 +5160,7 @@ for( key in config.keyboard ) { // Check if this binding matches the pressed key - if( parseInt( key, 10 ) === event.keyCode ) { + if( parseInt( key, 10 ) === keyCode ) { var value = config.keyboard[ key ]; @@ -4769,7 +5187,7 @@ for( key in registeredKeyBindings ) { // Check if this binding matches the pressed key - if( parseInt( key, 10 ) === event.keyCode ) { + if( parseInt( key, 10 ) === keyCode ) { var action = registeredKeyBindings[ key ].callback; @@ -4793,35 +5211,92 @@ // Assume true and try to prove false triggered = true; - switch( event.keyCode ) { - // p, page up - case 80: case 33: navigatePrev(); break; - // n, page down - case 78: case 34: navigateNext(); break; - // h, left - case 72: case 37: navigateLeft(); break; - // l, right - case 76: case 39: navigateRight(); break; - // k, up - case 75: case 38: navigateUp(); break; - // j, down - case 74: case 40: navigateDown(); break; - // home - case 36: slide( 0 ); break; - // end - case 35: slide( Number.MAX_VALUE ); break; - // space - case 32: isOverview() ? deactivateOverview() : event.shiftKey ? navigatePrev() : navigateNext(); break; - // return - case 13: isOverview() ? deactivateOverview() : triggered = false; break; - // two-spot, semicolon, b, v, period, Logitech presenter tools "black screen" button - case 58: case 59: case 66: case 86: case 190: case 191: togglePause(); break; - // f - case 70: enterFullscreen(); break; - // a - case 65: if ( config.autoSlideStoppable ) toggleAutoSlide( autoSlideWasPaused ); break; - default: - triggered = false; + // P, PAGE UP + if( keyCode === 80 || keyCode === 33 ) { + navigatePrev(); + } + // N, PAGE DOWN + else if( keyCode === 78 || keyCode === 34 ) { + navigateNext(); + } + // H, LEFT + else if( keyCode === 72 || keyCode === 37 ) { + if( firstSlideShortcut ) { + slide( 0 ); + } + else if( !isOverview() && config.navigationMode === 'linear' ) { + navigatePrev(); + } + else { + navigateLeft(); + } + } + // L, RIGHT + else if( keyCode === 76 || keyCode === 39 ) { + if( lastSlideShortcut ) { + slide( Number.MAX_VALUE ); + } + else if( !isOverview() && config.navigationMode === 'linear' ) { + navigateNext(); + } + else { + navigateRight(); + } + } + // K, UP + else if( keyCode === 75 || keyCode === 38 ) { + if( !isOverview() && config.navigationMode === 'linear' ) { + navigatePrev(); + } + else { + navigateUp(); + } + } + // J, DOWN + else if( keyCode === 74 || keyCode === 40 ) { + if( !isOverview() && config.navigationMode === 'linear' ) { + navigateNext(); + } + else { + navigateDown(); + } + } + // HOME + else if( keyCode === 36 ) { + slide( 0 ); + } + // END + else if( keyCode === 35 ) { + slide( Number.MAX_VALUE ); + } + // SPACE + else if( keyCode === 32 ) { + if( isOverview() ) { + deactivateOverview(); + } + if( event.shiftKey ) { + navigatePrev(); + } + else { + navigateNext(); + } + } + // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON + else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) { + togglePause(); + } + // F + else if( keyCode === 70 ) { + enterFullscreen(); + } + // A + else if( keyCode === 65 ) { + if ( config.autoSlideStoppable ) { + toggleAutoSlide( autoSlideWasPaused ); + } + } + else { + triggered = false; } } @@ -4832,7 +5307,7 @@ event.preventDefault && event.preventDefault(); } // ESC or O key - else if ( ( event.keyCode === 27 || event.keyCode === 79 ) && features.transforms3d ) { + else if ( ( keyCode === 27 || keyCode === 79 ) && features.transforms3d ) { if( dom.overlay ) { closeOverlay(); } @@ -4863,18 +5338,6 @@ touch.startY = event.touches[0].clientY; touch.startCount = event.touches.length; - // If there's two touches we need to memorize the distance - // between those two points to detect pinching - if( event.touches.length === 2 && config.overview ) { - touch.startSpan = distanceBetween( { - x: event.touches[1].clientX, - y: event.touches[1].clientY - }, { - x: touch.startX, - y: touch.startY - } ); - } - } /** @@ -4893,37 +5356,8 @@ var currentX = event.touches[0].clientX; var currentY = event.touches[0].clientY; - // If the touch started with two points and still has - // two active touches; test for the pinch gesture - if( event.touches.length === 2 && touch.startCount === 2 && config.overview ) { - - // The current distance in pixels between the two touch points - var currentSpan = distanceBetween( { - x: event.touches[1].clientX, - y: event.touches[1].clientY - }, { - x: touch.startX, - y: touch.startY - } ); - - // If the span is larger than the desire amount we've got - // ourselves a pinch - if( Math.abs( touch.startSpan - currentSpan ) > touch.threshold ) { - touch.captured = true; - - if( currentSpan < touch.startSpan ) { - activateOverview(); - } - else { - deactivateOverview(); - } - } - - event.preventDefault(); - - } // There was only one touch point, look for a swipe - else if( event.touches.length === 1 && touch.startCount !== 2 ) { + if( event.touches.length === 1 && touch.startCount !== 2 ) { var deltaX = currentX - touch.startX, deltaY = currentY - touch.startY; @@ -5073,8 +5507,8 @@ /** * Event handler for navigation control buttons. */ - function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); navigateLeft(); } - function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); navigateRight(); } + function onNavigateLeftClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); } + function onNavigateRightClicked( event ) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); } function onNavigateUpClicked( event ) { event.preventDefault(); onUserInput(); navigateUp(); } function onNavigateDownClicked( event ) { event.preventDefault(); onUserInput(); navigateDown(); } function onNavigatePrevClicked( event ) { event.preventDefault(); onUserInput(); navigatePrev(); } @@ -5464,6 +5898,10 @@ // Returns an Array of all slides getSlides: getSlides, + // Returns an Array of objects representing the attributes on + // the slides + getSlidesAttributes: getSlidesAttributes, + // Returns the total number of slides getTotalSlides: getTotalSlides, @@ -5514,6 +5952,16 @@ return query; }, + // Returns the top-level DOM element + getRevealElement: function() { + return dom.wrapper || document.querySelector( '.reveal' ); + }, + + // Returns a hash with all registered plugins + getPlugins: function() { + return plugins; + }, + // Returns true if we're currently on the first slide isFirstSlide: function() { return ( indexh === 0 && indexv === 0 ); @@ -5555,22 +6003,25 @@ // Forward event binding to the reveal DOM element addEventListener: function( type, listener, useCapture ) { if( 'addEventListener' in window ) { - ( dom.wrapper || document.querySelector( '.reveal' ) ).addEventListener( type, listener, useCapture ); + Reveal.getRevealElement().addEventListener( type, listener, useCapture ); } }, removeEventListener: function( type, listener, useCapture ) { if( 'addEventListener' in window ) { - ( dom.wrapper || document.querySelector( '.reveal' ) ).removeEventListener( type, listener, useCapture ); + Reveal.getRevealElement().removeEventListener( type, listener, useCapture ); } }, - // Adds a custom key binding + // Adds/removes a custom key binding addKeyBinding: addKeyBinding, - - // Removes a custom key binding removeKeyBinding: removeKeyBinding, - // Programatically triggers a keyboard event + // API for registering and retrieving plugins + registerPlugin: registerPlugin, + hasPlugin: hasPlugin, + getPlugin: getPlugin, + + // Programmatically triggers a keyboard event triggerKey: function( keyCode ) { onDocumentKeyDown( { keyCode: keyCode } ); }, From c038476b80777d94ea7288c419871ab02171bf14 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Mon, 8 Apr 2019 23:06:05 +0200 Subject: [PATCH 39/39] Fix songstab --- openlp/plugins/songs/lib/songstab.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/plugins/songs/lib/songstab.py b/openlp/plugins/songs/lib/songstab.py index cbafa9c40..d08a1b470 100644 --- a/openlp/plugins/songs/lib/songstab.py +++ b/openlp/plugins/songs/lib/songstab.py @@ -122,7 +122,6 @@ class SongsTab(SettingsTab): 'Import missing songs from Service files')) self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab', 'Add Songbooks as first slide')) - self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer')) self.chords_info_label.setText(translate('SongsPlugin.SongsTab', 'If enabled all text between "[" and "]" will ' 'be regarded as chords.')) self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
{ph}{desc}
{{{{{pl}}}}}{des}{opt}
${{{pl}}}{des}{opt}