This commit is contained in:
Tim Bentley 2019-04-09 19:35:10 +01:00
commit 49384aa672
25 changed files with 1416 additions and 686 deletions

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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 = '<span class="chordline">{chord}</span>'
FIRST_CHORD_TEMPLATE = '<span class="chordline firstchordline">{chord}</span>'
CHORD_LINE_TEMPLATE = '<span class="chord"><span><strong>{chord}</strong></span></span>{tail}{whitespace}{remainder}'

View File

@ -81,7 +81,8 @@ class ServiceItem(RegistryProperties):
self.items = []
self.icon = UiIcons().default
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.unique_identifier = 0
@ -165,7 +166,8 @@ class ServiceItem(RegistryProperties):
# the dict instead of rendering them again.
previous_pages = {}
index = 0
self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
if not self.footer_html:
self.footer_html = '<br>'.join([_f for _f in self.raw_footer if _f])
for raw_slide in self.slides:
verse_tag = raw_slide['verse']
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
@ -178,7 +180,7 @@ class ServiceItem(RegistryProperties):
'title': raw_slide['title'],
'text': render_tags(page),
'verse': index,
'footer': self.foot_text,
'footer': self.footer_html,
}
self._rendered_slides.append(rendered_slide)
display_slide = {

View File

@ -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')

View File

@ -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
@ -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,92 @@ 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_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):
"""
@ -330,23 +361,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 +387,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
@ -432,54 +446,31 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
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):
"""

View File

@ -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
@ -39,14 +38,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 +97,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 +114,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 +139,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 +248,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('<span style="font-size:14pt; font-weight:600;">{text}'
'</span>'.format(text=text))
button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.NextButton))
first_time_wizard.information_label.setText(
self.title_label.setText('<span style="font-size:14pt; font-weight:600;">{text}</span>'.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 +281,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 +293,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,17 +323,5 @@ 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)))
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)
self.progress_page.setSubTitle(
translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded and OpenLP is configured.'))

View File

@ -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()

View File

@ -235,11 +235,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('<br>')[2]
if foot_text:
foot_text = html.escape(foot_text.replace('<br>', '\n'))
self._add_element('div', foot_text.replace('\n', '<br>'), parent=div, class_id='itemFooter')
footer_html = item.footer_html
footer_html = footer_html.partition('<br>')[2]
if footer_html:
footer_html = html.escape(footer_html.replace('<br>', '\n'))
self._add_element('div', footer_html.replace('\n', '<br>'), parent=div, classId='itemFooter')
# Add service items' notes.
if self.notes_check_box.isChecked():
if item.notes:

View File

@ -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

View File

@ -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

View File

@ -21,6 +21,7 @@
###############################################################################
import logging
import os
import mako
from PyQt5 import QtCore, QtWidgets
from sqlalchemy.sql import and_, or_
@ -35,7 +36,7 @@ from openlp.core.lib import ServiceItemContext, check_item_selected, create_sepa
from openlp.core.lib.mediamanageritem import MediaManagerItem
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.serviceitem import ItemCapabilities
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.ui.icons import UiIcons
from openlp.plugins.songs.forms.editsongform import EditSongForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
@ -131,9 +132,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 retranslate_ui(self):
self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
@ -583,9 +581,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)
@ -675,12 +675,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)))
@ -694,34 +690,44 @@ 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.metadata.append('<em>{label}:</em> {title}'.format(label=translate('SongsPlugin.MediaItem', 'Title'),
title=song.title))
if song.alternate_title:
item.metadata.append('<em>{label}:</em> {title}'.
format(label=translate('SongsPlugin.MediaItem', 'Alt Title'),
title=song.alternate_title))
if song.songbook_entries:
for songbook_entry in song.songbook_entries:
item.metadata.append('<em>{label}:</em> {book}/{num}/{pub}'.
format(label=translate('SongsPlugin.MediaItem', 'Songbook'),
book=songbook_entry.songbook.name,
num=songbook_entry.entry,
pub=songbook_entry.songbook.publisher))
if song.topics:
for topics in song.topics:
item.metadata.append('<em>{label}:</em> {topic}'.
format(label=translate('SongsPlugin.MediaItem', 'Topic'), topic=topics.name))
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 = {
'title': song.title,
'alternate_title': song.alternate_title,
'authors_none_label': translate('OpenLP.Ui', 'Written by'),
'authors_none': authors_none,
'authors_words_label': AuthorType.Types[AuthorType.Words],
'authors_words': authors_words,
'authors_music_label': AuthorType.Types[AuthorType.Music],
'authors_music': authors_music,
'authors_words_music_label': AuthorType.Types[AuthorType.WordsAndMusic],
'authors_words_music': authors_words_music,
'authors_translation_label': AuthorType.Types[AuthorType.Translation],
'authors_translation': authors_translation,
'authors_words_all': authors_words + authors_words_music,
'authors_music_all': authors_music + authors_words_music,
'copyright': song.copyright,
'songbook_entries': songbooks,
'ccli_license': Settings().value('core/ccli number'),
'ccli_license_label': translate('SongsPlugin.MediaItem', 'CCLI License'),
'ccli_number': song.ccli_number,
'topics': [topic.name for topic in song.topics]
}
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):

View File

@ -25,7 +25,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.db import AuthorType
class SongsTab(SettingsTab):
@ -54,15 +54,6 @@ class SongsTab(SettingsTab):
self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)
self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')
self.mode_layout.addWidget(self.songbook_slide_check_box)
self.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)
@ -93,20 +84,34 @@ 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.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_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 retranslate_ui(self):
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Song related settings'))
@ -116,13 +121,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'))
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))
'Add Songbooks as first slide'))
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'))
@ -134,6 +133,53 @@ 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 = '<code>"{}"</code>'
placeholders = [
# placeholder, description, can be empty, is a list
['title', translate('SongsPlugin.SongsTab', 'Song 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],
['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_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_translation_label', const.format(AuthorType.Types[AuthorType.Translation]), False, False],
['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],
['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],
]
placeholder_info = '<table style="background: #eee">\n<tr><th><b>{ph}</b></th><th><b>{desc}</b></th></tr>\n'\
.format(ph=translate('SongsPlugin.SongsTab', 'Placeholder'),
desc=translate('SongsPlugin.SongsTab', 'Description'))
for placeholder in placeholders:
placeholder_info += '<tr><td>${{{pl}}}</td><td>{des}{opt}</td></tr>\n'\
.format(pl=placeholder[0], des=placeholder[1],
opt=('&nbsp;¹' if placeholder[2] else '') +
('&nbsp;²' if placeholder[3] else ''))
placeholder_info += '</table>'
placeholder_info += '\n<br/>¹ {}'.format(translate('SongsPlugin.SongsTab', 'can be empty'))
placeholder_info += '\n<br/>² {}'.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('{} (<a href="http://docs.makotemplates.org">{}</a>):'
.format(translate('SongsPlugin.SongsTab', 'Footer Template'),
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):
self.song_search = (check_state == QtCore.Qt.Checked)
@ -150,15 +196,6 @@ class SongsTab(SettingsTab):
def on_songbook_slide_check_box_changed(self, check_state):
self.songbook_slide = (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)
@ -174,6 +211,9 @@ 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)
@ -181,9 +221,6 @@ class SongsTab(SettingsTab):
self.update_edit = settings.value('update service on edit')
self.update_load = settings.value('add song from service')
self.songbook_slide = settings.value('add songbook slide')
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')
@ -191,9 +228,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)
@ -203,6 +237,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):
@ -211,13 +246,13 @@ 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)
# 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.setValue('add songbook slide', self.songbook_slide)
settings.endGroup()
if self.tab_visited:

View File

@ -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:

View File

@ -66,11 +66,8 @@ __default_settings__ = {
'songs/add song from service': True,
'songs/add songbook slide': False,
'songs/display songbar': True,
'songs/display songbook': False,
'songs/display written by': True,
'songs/display copyright symbol': False,
'songs/last directory import': None,
'songs/last directory export': None,
'songs/last directory import': '',
'songs/last directory export': '',
'songs/songselect username': '',
'songs/songselect password': '',
'songs/songselect searches': '',
@ -78,6 +75,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}<br/>
%if authors_none:
<%
authors = ", ".join(authors_none)
%>
${authors_none_label}:&nbsp;${authors}<br/>
%endif
%if authors_words_music:
<%
authors = ", ".join(authors_words_music)
%>
${authors_words_music_label}:&nbsp;${authors}<br/>
%endif
%if authors_words:
<%
authors = ", ".join(authors_words)
%>
${authors_words_label}:&nbsp;${authors}<br/>
%endif
%if authors_music:
<%
authors = ", ".join(authors_music)
%>
${authors_music_label}:&nbsp;${authors}<br/>
%endif
%if authors_translation:
<%
authors = ", ".join(authors_translation)
%>
${authors_translation_label}:&nbsp;${authors}<br/>
%endif
%if copyright:
&copy;&nbsp;${copyright}<br/>
%endif
%if songbook_entries:
<%
entries = ", ".join(songbook_entries)
%>
${entries}<br/>
%endif
%if ccli_license:
${ccli_license_label}&nbsp;${ccli_license}<br/>
%endif
""",
}

0
run_openlp.py Executable file → Normal file
View File

13
setup.py Executable file → Normal file
View File

@ -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']}
)

View File

@ -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')
@ -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):

View File

@ -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')

View File

@ -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
@ -45,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'}
@ -59,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())
@ -74,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 = []
@ -120,10 +123,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.firsttimeform.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 +150,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 +167,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 +187,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 +208,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 +223,78 @@ 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):
"""
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.firsttimeform.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.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')
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):
@ -279,10 +379,10 @@ 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')
@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
@ -300,7 +400,7 @@ class TestFirstTimeForm(TestCase, TestMixin):
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

View File

@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch
from PyQt5 import QtCore, QtGui
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
@ -49,6 +50,25 @@ 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
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
class TestPdfController(TestCase, TestMixin):
"""
Test the PdfController.
@ -137,8 +157,11 @@ 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()
# Calculate the width of the PDF based on the aspect ratio of the PDF
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)
@patch('openlp.plugins.presentations.lib.pdfcontroller.check_binary_exists')
def test_process_check_binary_mudraw(self, mocked_check_binary_exists):

View File

@ -34,6 +34,62 @@ from openlp.plugins.songs.lib.db import AuthorType, Song
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from tests.helpers.testmixin import TestMixin
__default_settings__ = {
'songs/footer template': """
${title}<br/>
%if authors_none:
<%
authors = ", ".join(authors_none)
%>
${authors_none_label}:&nbsp;${authors}<br/>
%endif
%if authors_words_music:
<%
authors = ", ".join(authors_words_music)
%>
${authors_words_music_label}:&nbsp;${authors}<br/>
%endif
%if authors_words:
<%
authors = ", ".join(authors_words)
%>
${authors_words_label}:&nbsp;${authors}<br/>
%endif
%if authors_music:
<%
authors = ", ".join(authors_music)
%>
${authors_music_label}:&nbsp;${authors}<br/>
%endif
%if authors_translation:
<%
authors = ", ".join(authors_translation)
%>
${authors_translation_label}:&nbsp;${authors}<br/>
%endif
%if copyright:
&copy;&nbsp;${copyright}<br/>
%endif
%if songbook_entries:
<%
entries = ", ".join(songbook_entries)
%>
${entries}<br/>
%endif
%if ccli_license:
${ccli_license_label}&nbsp;${ccli_license}<br/>
%endif
"""
}
class TestMediaItem(TestCase, TestMixin):
"""
@ -61,6 +117,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):
@ -297,63 +354,45 @@ 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 = [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('mako.template.Template.render_unicode') 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'
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)
# 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
assert 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'
assert author_list == ['my author'], 'The author list should be returned correctly with one author'
@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
assert 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.'
assert author_list == ['my author'], 'The author list should be returned correctly with one author'
# 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')
def test_build_song_footer_two_authors(self):
"""
@ -382,6 +421,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
@ -389,7 +429,7 @@ class TestMediaItem(TestCase, TestMixin):
# THEN: I get the following Array returned
assert 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'
assert author_list == ['another author', 'my author', 'translator'], \
'The author list should be returned correctly with two authors'
@ -402,6 +442,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')
@ -409,7 +450,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 1234'], \
assert 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
@ -417,7 +458,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I would get an amended footer string
assert service_item.raw_footer == ['My Song', 'My copyright', 'CCLI License: 4321'], \
assert 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):
@ -431,6 +472,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'
@ -444,15 +487,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
assert 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
assert service_item.raw_footer == ['My Song', 'My copyright', 'My songbook #12, Thy songbook #502A']
assert service_item.raw_footer == ['My Song', '© My copyright', 'My songbook #12, Thy songbook #502A']
def test_build_song_footer_copyright_enabled(self):
"""
@ -463,6 +499,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
@ -479,13 +516,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
assert service_item.raw_footer == ['My Song', 'My copyright']
assert service_item.raw_footer == ['My Song', '© My copyright']
def test_authors_match(self):
"""

View File

@ -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'

View File

@ -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