working webview implementation of the ccli importer

Still a WIP:
 - I'll add the old preview thing
 - Need to make default size of the window larger
 - Need to make UI look better
This commit is contained in:
Daniel 2020-08-06 02:26:56 +00:00 committed by Raoul Snyman
parent a036362237
commit 483f14ec15
7 changed files with 968 additions and 1194 deletions

View File

@ -27,7 +27,7 @@ from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.ui import SingleColumnTableWidget
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.edits import HistoryComboBox
from openlp.plugins.songs.forms.webengine import WebEngineView
class Ui_SongSelectDialog(object):
@ -36,122 +36,23 @@ class Ui_SongSelectDialog(object):
"""
def setup_ui(self, songselect_dialog):
songselect_dialog.setObjectName('songselect_dialog')
songselect_dialog.resize(616, 378)
songselect_dialog.resize(800, 600)
self.songselect_layout = QtWidgets.QVBoxLayout(songselect_dialog)
self.songselect_layout.setSpacing(0)
self.songselect_layout.setContentsMargins(0, 0, 0, 0)
self.songselect_layout.setSpacing(8)
self.songselect_layout.setContentsMargins(8, 8, 8, 8)
self.songselect_layout.setObjectName('songselect_layout')
self.stacked_widget = QtWidgets.QStackedWidget(songselect_dialog)
self.stacked_widget.setObjectName('stacked_widget')
self.login_page = QtWidgets.QWidget()
self.login_page.setObjectName('login_page')
self.login_layout = QtWidgets.QFormLayout(self.login_page)
self.login_layout.setContentsMargins(120, 100, 120, 100)
self.login_layout.setSpacing(8)
self.login_layout.setObjectName('login_layout')
self.notice_layout = QtWidgets.QHBoxLayout()
self.notice_layout.setObjectName('notice_layout')
self.notice_label = QtWidgets.QLabel(self.login_page)
self.notice_label.setWordWrap(True)
self.notice_label.setObjectName('notice_label')
self.notice_layout.addWidget(self.notice_label)
self.login_layout.setLayout(0, QtWidgets.QFormLayout.SpanningRole, self.notice_layout)
self.username_label = QtWidgets.QLabel(self.login_page)
self.username_label.setObjectName('usernameLabel')
self.login_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.username_label)
self.username_edit = QtWidgets.QLineEdit(self.login_page)
self.username_edit.setObjectName('usernameEdit')
self.login_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.username_edit)
self.password_label = QtWidgets.QLabel(self.login_page)
self.password_label.setObjectName('passwordLabel')
self.login_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.password_label)
self.password_edit = QtWidgets.QLineEdit(self.login_page)
self.password_edit.setEchoMode(QtWidgets.QLineEdit.Password)
self.password_edit.setObjectName('passwordEdit')
self.login_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.password_edit)
self.save_password_checkbox = QtWidgets.QCheckBox(self.login_page)
self.save_password_checkbox.setTristate(False)
self.save_password_checkbox.setObjectName('save_password_checkbox')
self.login_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.save_password_checkbox)
self.login_button_layout = QtWidgets.QHBoxLayout()
self.login_button_layout.setSpacing(8)
self.login_button_layout.setContentsMargins(0, -1, -1, -1)
self.login_button_layout.setObjectName('login_button_layout')
self.login_spacer = QtWidgets.QWidget(self.login_page)
self.login_spacer.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.login_spacer.setObjectName('login_spacer')
self.login_button_layout.addWidget(self.login_spacer)
self.login_progress_bar = QtWidgets.QProgressBar(self.login_page)
self.login_progress_bar.setMinimum(0)
self.login_progress_bar.setMaximum(3)
self.login_progress_bar.setValue(0)
self.login_progress_bar.setMinimumWidth(200)
self.login_progress_bar.setVisible(False)
self.login_button_layout.addWidget(self.login_progress_bar)
self.login_button = QtWidgets.QPushButton(self.login_page)
self.login_button.setIcon(UiIcons().edit)
self.login_button.setObjectName('login_button')
self.login_button_layout.addWidget(self.login_button)
self.login_layout.setLayout(4, QtWidgets.QFormLayout.SpanningRole, self.login_button_layout)
self.stacked_widget.addWidget(self.login_page)
self.search_page = QtWidgets.QWidget()
self.search_page.setObjectName('search_page')
self.search_layout = QtWidgets.QVBoxLayout(self.search_page)
self.search_layout.setSpacing(8)
self.search_layout.setContentsMargins(8, 8, 8, 8)
self.search_layout.setObjectName('search_layout')
self.search_input_layout = QtWidgets.QHBoxLayout()
self.search_input_layout.setSpacing(8)
self.search_input_layout.setObjectName('search_input_layout')
self.search_label = QtWidgets.QLabel(self.search_page)
self.search_label.setObjectName('search_label')
self.search_input_layout.addWidget(self.search_label)
self.search_combobox = HistoryComboBox(self.search_page)
self.search_combobox.setObjectName('search_combobox')
self.search_input_layout.addWidget(self.search_combobox)
self.search_button = QtWidgets.QPushButton(self.search_page)
self.search_button.setIcon(UiIcons().search)
self.search_button.setObjectName('search_button')
self.search_input_layout.addWidget(self.search_button)
self.search_layout.addLayout(self.search_input_layout)
self.search_progress_layout = QtWidgets.QHBoxLayout()
self.search_progress_layout.setSpacing(8)
self.search_progress_layout.setObjectName('search_progress_layout')
self.search_progress_bar = QtWidgets.QProgressBar(self.search_page)
self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(3)
self.search_progress_bar.setValue(0)
self.search_progress_layout.addWidget(self.search_progress_bar)
self.stop_button = QtWidgets.QPushButton(self.search_page)
self.stop_button.setIcon(UiIcons().stop)
self.stop_button.setObjectName('stop_button')
self.search_progress_layout.addWidget(self.stop_button)
self.search_layout.addLayout(self.search_progress_layout)
self.search_results_widget = QtWidgets.QListWidget(self.search_page)
self.search_results_widget.setProperty("showDropIndicator", False)
self.search_results_widget.setAlternatingRowColors(True)
self.search_results_widget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.search_results_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.search_results_widget.setObjectName('search_results_widget')
self.search_layout.addWidget(self.search_results_widget)
self.result_count_label = QtWidgets.QLabel(self.search_page)
self.result_count_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter)
self.result_count_label.setObjectName('result_count_label')
self.search_layout.addWidget(self.result_count_label)
self.view_layout = QtWidgets.QHBoxLayout()
self.view_layout.setSpacing(8)
self.view_layout.setObjectName('view_layout')
self.logout_button = QtWidgets.QPushButton(self.search_page)
self.logout_button.setIcon(UiIcons().edit)
self.view_layout.addWidget(self.logout_button)
self.view_spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.view_layout.addItem(self.view_spacer)
self.view_button = QtWidgets.QPushButton(self.search_page)
self.view_button.setIcon(UiIcons().search)
self.view_button.setObjectName('view_button')
self.view_layout.addWidget(self.view_button)
self.search_layout.addLayout(self.view_layout)
self.stacked_widget.addWidget(self.search_page)
# Webview page
self.webview_page = QtWidgets.QWidget()
self.webview_page.setObjectName('webview_page')
self.webview_layout = QtWidgets.QGridLayout(self.webview_page)
self.webview_layout.setObjectName('webview_layout')
self.webview_layout.setContentsMargins(0, 0, 0, 0)
self.webview = WebEngineView(self)
self.webview_layout.addWidget(self.webview, 1, 0, 3, 1)
self.stacked_widget.addWidget(self.webview_page)
# Song page
self.song_page = QtWidgets.QWidget()
self.song_page.setObjectName('song_page')
self.song_layout = QtWidgets.QGridLayout(self.song_page)
@ -193,36 +94,54 @@ class Ui_SongSelectDialog(object):
self.lyrics_table_widget.setObjectName('lyrics_table_widget')
self.lyrics_table_widget.setRowCount(0)
self.song_layout.addWidget(self.lyrics_table_widget, 3, 1, 1, 3)
self.song_progress_bar = QtWidgets.QProgressBar(self.song_page)
self.song_progress_bar.setMinimum(0)
self.song_progress_bar.setMaximum(3)
self.song_progress_bar.setValue(0)
self.song_progress_bar.setVisible(False)
self.song_layout.addWidget(self.song_progress_bar, 4, 0, 1, 4)
self.import_layout = QtWidgets.QHBoxLayout()
self.import_layout.setObjectName('import_layout')
self.back_button = QtWidgets.QPushButton(self.song_page)
self.back_button.setIcon(UiIcons().back)
self.back_button.setObjectName('back_button')
self.import_layout.addWidget(self.back_button)
self.import_spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.import_layout.addItem(self.import_spacer)
self.import_button = QtWidgets.QPushButton(self.song_page)
self.import_button.setIcon(UiIcons().download)
self.import_button.setObjectName('import_button')
self.import_layout.addWidget(self.import_button)
self.song_layout.addLayout(self.import_layout, 5, 0, 1, 5)
self.stacked_widget.addWidget(self.song_page)
self.songselect_layout.addWidget(self.stacked_widget)
self.username_label.setBuddy(self.username_edit)
self.password_label.setBuddy(self.password_edit)
self.title_label.setBuddy(self.title_edit)
self.authors_label.setBuddy(self.author_list_widget)
self.copyright_label.setBuddy(self.copyright_edit)
self.ccli_label.setBuddy(self.ccli_edit)
self.lyrics_label.setBuddy(self.lyrics_table_widget)
self.stacked_widget.addWidget(self.song_page)
# The top panel
self.top_button_layout = QtWidgets.QGridLayout()
self.top_button_layout.setContentsMargins(0, 0, 0, 0)
self.top_button_layout.setSpacing(8)
self.top_button_layout.setObjectName('top_button_layout')
self.back_button = QtWidgets.QPushButton(songselect_dialog)
self.back_button.setIcon(UiIcons().back)
self.back_button.setObjectName('back_button')
self.top_button_layout.addWidget(self.back_button, 0, 0, 1, 1)
self.url_bar = QtWidgets.QLineEdit(songselect_dialog)
self.url_bar.setObjectName('ccli_edit')
self.top_button_layout.addWidget(self.url_bar, 0, 1, 1, 8)
self.song_progress_bar = QtWidgets.QProgressBar(songselect_dialog)
self.song_progress_bar.setMinimum(0)
self.song_progress_bar.setMaximum(3)
self.song_progress_bar.setValue(0)
self.song_progress_bar.setVisible(False)
self.top_button_layout.addWidget(self.song_progress_bar, 0, 1, 1, 8)
# The bottom panel
self.bottom_button_layout = QtWidgets.QGridLayout()
self.bottom_button_layout.setContentsMargins(0, 0, 0, 0)
self.bottom_button_layout.setSpacing(8)
self.bottom_button_layout.setObjectName('bottom_button_layout')
self.close_button = QtWidgets.QPushButton(songselect_dialog)
self.close_button.setIcon(UiIcons().close)
self.close_button.setObjectName('close_button')
self.bottom_button_layout.addWidget(self.close_button, 0, 0, 1, 1)
self.spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
self.bottom_button_layout.addItem(self.spacer, 0, 1, 1, 7)
self.view_button = QtWidgets.QPushButton(songselect_dialog)
self.view_button.setIcon(UiIcons().search)
self.view_button.setObjectName('view_button')
self.bottom_button_layout.addWidget(self.view_button, 0, 8, 1, 1)
self.import_button = QtWidgets.QPushButton(songselect_dialog)
self.import_button.setIcon(UiIcons().download)
self.import_button.setObjectName('import_button')
self.bottom_button_layout.addWidget(self.import_button, 0, 9, 1, 1)
# Add everything to the interface layout
self.songselect_layout.addLayout(self.top_button_layout)
self.songselect_layout.addWidget(self.stacked_widget)
self.songselect_layout.addLayout(self.bottom_button_layout)
self.retranslate_ui(songselect_dialog)
self.stacked_widget.setCurrentIndex(0)
@ -231,25 +150,13 @@ class Ui_SongSelectDialog(object):
Translate the GUI.
"""
songselect_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'CCLI SongSelect Importer'))
self.notice_label.setText(
translate('SongsPlugin.SongSelectForm', '<strong>Note:</strong> '
'An Internet connection is required in order to import songs from CCLI SongSelect.')
)
self.username_label.setText(translate('SongsPlugin.SongSelectForm', 'Username:'))
self.password_label.setText(translate('SongsPlugin.SongSelectForm', 'Password:'))
self.save_password_checkbox.setText(translate('SongsPlugin.SongSelectForm', 'Save username and password'))
self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login'))
self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:'))
self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search'))
self.stop_button.setText(translate('SongsPlugin.SongSelectForm', 'Stop'))
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
'Found {count:d} song(s)').format(count=0))
self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout'))
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View'))
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'Preview'))
self.title_label.setText(translate('SongsPlugin.SongSelectForm', 'Title:'))
self.authors_label.setText(translate('SongsPlugin.SongSelectForm', 'Author(s):'))
self.copyright_label.setText(translate('SongsPlugin.SongSelectForm', 'Copyright:'))
self.ccli_label.setText(translate('SongsPlugin.SongSelectForm', 'CCLI Number:'))
self.lyrics_label.setText(translate('SongsPlugin.SongSelectForm', 'Lyrics:'))
self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back'))
self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back',
'When pressed takes user to the CCLI home page'))
self.import_button.setText(translate('SongsPlugin.SongSelectForm', 'Import'))
self.close_button.setText(translate('SongsPlugin.SongSelectForm', 'Close'))

View File

@ -22,56 +22,20 @@
The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer
"""
import logging
from time import sleep
from PyQt5 import QtCore, QtWidgets
from sqlalchemy.sql import and_
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.threading import ThreadWorker, run_thread
from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog
from openlp.plugins.songs.lib.songselect import SongSelectImport
from openlp.plugins.songs.lib.db import Song
from openlp.plugins.songs.lib.songselect import SongSelectImport, Pages
log = logging.getLogger(__name__)
class SearchWorker(ThreadWorker):
"""
Run the actual SongSelect search, and notify the GUI when we find each song.
"""
show_info = QtCore.pyqtSignal(str, str)
found_song = QtCore.pyqtSignal(dict)
finished = QtCore.pyqtSignal()
def __init__(self, importer, search_text):
super().__init__()
self.importer = importer
self.search_text = search_text
def start(self):
"""
Run a search and then parse the results page of the search.
"""
songs = self.importer.search(self.search_text, 1000, self._found_song_callback)
if len(songs) >= 1000:
self.show_info.emit(
translate('SongsPlugin.SongSelectForm', 'More than 1000 results'),
translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has '
'been stopped. Please refine your search to fetch better '
'results.'))
self.finished.emit()
self.quit.emit()
def _found_song_callback(self, song):
"""
A callback used by the paginate function to notify watching processes when it finds a song.
:param song: The song that was found
"""
self.found_song.emit(song)
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties):
"""
The :class:`SongSelectForm` class is the SongSelect dialog.
@ -88,43 +52,26 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
"""
Initialise the SongSelectForm
"""
self.song_count = 0
self.song = None
self.set_progress_visible(False)
self.song_select_importer = SongSelectImport(self.db_manager)
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
self.login_button.clicked.connect(self.on_login_button_clicked)
self.search_button.clicked.connect(self.on_search_button_clicked)
self.search_combobox.returnPressed.connect(self.on_search_button_clicked)
self.stop_button.clicked.connect(self.on_stop_button_clicked)
self.logout_button.clicked.connect(self.done)
self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked)
self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed)
self.song_select_importer = SongSelectImport(self.db_manager, self.webview)
self.url_bar.returnPressed.connect(self.on_url_bar_return_pressed)
self.view_button.clicked.connect(self.on_view_button_clicked)
self.back_button.clicked.connect(self.on_back_button_clicked)
self.close_button.clicked.connect(self.done)
self.import_button.clicked.connect(self.on_import_button_clicked)
self.webview.page().loadStarted.connect(self.page_load_started)
self.webview.page().loadFinished.connect(self.page_loaded)
def exec(self):
"""
Execute the dialog. This method sets everything back to its initial
values.
"""
self.stacked_widget.setCurrentIndex(0)
self.username_edit.setEnabled(True)
self.password_edit.setEnabled(True)
self.save_password_checkbox.setEnabled(True)
self.search_combobox.clearEditText()
self.search_combobox.clear()
self.search_results_widget.clear()
self.song_select_importer.reset_webview()
self.view_button.setEnabled(False)
if self.settings.contains('songs/songselect password'):
self.username_edit.setText(self.settings.value('songs/songselect username'))
self.password_edit.setText(self.settings.value('songs/songselect password'))
self.save_password_checkbox.setChecked(True)
if self.settings.contains('songs/songselect searches'):
self.search_combobox.addItems(
self.settings.value('songs/songselect searches').split('|'))
self.username_edit.setFocus()
self.back_button.setEnabled(False)
self.import_button.setEnabled(False)
self.stacked_widget.setCurrentIndex(0)
return QtWidgets.QDialog.exec(self)
def done(self, result_code):
@ -133,279 +80,127 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
:param result_code: The result of the dialog.
"""
log.debug('Closing SongSelectForm')
if self.stacked_widget.currentIndex() > 0:
progress_dialog = QtWidgets.QProgressDialog(
translate('SongsPlugin.SongSelectForm', 'Logging out...'), '', 0, 2, self)
progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
progress_dialog.setCancelButton(None)
progress_dialog.setValue(1)
progress_dialog.show()
progress_dialog.setFocus()
self.application.process_events()
sleep(0.5)
self.application.process_events()
self.song_select_importer.logout()
self.application.process_events()
progress_dialog.setValue(2)
return QtWidgets.QDialog.done(self, result_code)
def _update_login_progress(self):
"""
Update the progress bar as the user logs in.
"""
self.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
self.application.process_events()
def page_load_started(self):
self.song_progress_bar.setMaximum(0)
self.song_progress_bar.setValue(0)
self.song_progress_bar.setVisible(True)
self.url_bar.setVisible(False)
self.import_button.setEnabled(False)
self.view_button.setEnabled(False)
self.back_button.setEnabled(False)
def page_loaded(self, successful):
self.song = None
page_type = self.song_select_importer.get_page_type()
if page_type == Pages.Login:
self.signin_page_loaded()
elif page_type == Pages.Song:
self.song_progress_bar.setMaximum(3)
self.song_progress_bar.setValue(0)
self.song = self.song_select_importer.get_song(self._update_song_progress)
self.import_button.setEnabled(True)
self.view_button.setEnabled(True)
self.back_button.setEnabled(True)
if page_type == Pages.Other:
self.back_button.setEnabled(True)
self.song_progress_bar.setVisible(False)
self.url_bar.setText(self.webview.url().toString())
self.url_bar.setCursorPosition(0)
self.url_bar.setVisible(True)
def signin_page_loaded(self):
username = self.settings.value('songs/songselect username')
password = self.settings.value('songs/songselect password')
self.song_select_importer.set_login_fields(username, password)
def _update_song_progress(self):
"""
Update the progress bar as the song is being downloaded.
Update the progress bar.
"""
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
self.application.process_events()
def _view_song(self, current_item):
def _view_song(self):
"""
Load a song into the song view.
"""
if not current_item:
return
else:
current_item = current_item.data(QtCore.Qt.UserRole)
# Stop the current search, if it's running
self.song_select_importer.stop()
# Clear up the UI
self.song_progress_bar.setVisible(True)
self.import_button.setEnabled(False)
self.back_button.setEnabled(False)
self.title_edit.setText('')
self.title_edit.setEnabled(False)
self.copyright_edit.setText('')
self.copyright_edit.setEnabled(False)
self.ccli_edit.setText('')
self.ccli_edit.setEnabled(False)
self.author_list_widget.clear()
self.author_list_widget.setEnabled(False)
self.lyrics_table_widget.clear()
self.lyrics_table_widget.setRowCount(0)
self.lyrics_table_widget.setEnabled(False)
self.stacked_widget.setCurrentIndex(2)
song = {}
for key, value in current_item.items():
song[key] = value
self.song_progress_bar.setValue(0)
self.application.process_events()
# Get the full song
song = self.song_select_importer.get_song(song, self._update_song_progress)
if not song:
if not self.song:
QtWidgets.QMessageBox.critical(
self, translate('SongsPlugin.SongSelectForm', 'Incomplete song'),
translate('SongsPlugin.SongSelectForm', 'This song is missing some information, like the lyrics, '
'and cannot be imported.'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok), QtWidgets.QMessageBox.Ok)
self.stacked_widget.setCurrentIndex(1)
return
# Clear up the UI
self.author_list_widget.clear()
self.lyrics_table_widget.clear()
self.lyrics_table_widget.setRowCount(0)
# Update the UI
self.title_edit.setText(song['title'])
self.copyright_edit.setText(song['copyright'])
self.ccli_edit.setText(song['ccli_number'])
for author in song['authors']:
QtWidgets.QListWidgetItem(author, self.author_list_widget)
for counter, verse in enumerate(song['verses']):
self.title_edit.setText(self.song['title'])
self.copyright_edit.setText(self.song['copyright'])
self.ccli_edit.setText(self.song['ccli_number'])
for author in self.song['authors']:
self.author_list_widget.addItem(QtWidgets.QListWidgetItem(author, self.author_list_widget))
for counter, verse in enumerate(self.song['verses']):
self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1)
item = QtWidgets.QTableWidgetItem(verse['lyrics'])
item.setData(QtCore.Qt.UserRole, verse['label'])
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
self.lyrics_table_widget.setItem(counter, 0, item)
self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in song['verses']])
self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in self.song['verses']])
self.lyrics_table_widget.resizeRowsToContents()
self.title_edit.setEnabled(True)
self.copyright_edit.setEnabled(True)
self.ccli_edit.setEnabled(True)
self.author_list_widget.setEnabled(True)
self.lyrics_table_widget.setEnabled(True)
self.lyrics_table_widget.repaint()
self.import_button.setEnabled(True)
self.back_button.setEnabled(True)
self.song_progress_bar.setVisible(False)
self.song_progress_bar.setValue(0)
self.song = song
self.application.process_events()
self.lyrics_table_widget.scrollToTop()
self.stacked_widget.setCurrentIndex(1)
def on_save_password_checkbox_toggled(self, checked):
def on_url_bar_return_pressed(self):
"""
Show a warning dialog when the user toggles the save checkbox on or off.
:param checked: If the combobox is checked or not
Go to the url in the url bar
"""
if checked and self.login_page.isVisible():
answer = QtWidgets.QMessageBox.question(
self, translate('SongsPlugin.SongSelectForm', 'Save Username and Password'),
translate('SongsPlugin.SongSelectForm', 'WARNING: Saving your username and password is INSECURE, your '
'password is stored in PLAIN TEXT. Click Yes to save your '
'password or No to cancel this.'),
defaultButton=QtWidgets.QMessageBox.No)
if answer == QtWidgets.QMessageBox.No:
self.save_password_checkbox.setChecked(False)
def on_login_button_clicked(self):
"""
Log the user in to SongSelect.
"""
self.username_edit.setEnabled(False)
self.password_edit.setEnabled(False)
self.save_password_checkbox.setEnabled(False)
self.login_button.setEnabled(False)
self.login_spacer.setVisible(False)
self.login_progress_bar.setValue(0)
self.login_progress_bar.setVisible(True)
self.application.process_events()
# Log the user in
subscription_level = self.song_select_importer.login(
self.username_edit.text(), self.password_edit.text(), self._update_login_progress)
if not subscription_level:
QtWidgets.QMessageBox.critical(
self,
translate('SongsPlugin.SongSelectForm', 'Error Logging In'),
translate('SongsPlugin.SongSelectForm',
'There was a problem logging in, perhaps your username or password is incorrect?')
)
else:
if subscription_level == 'Free':
QtWidgets.QMessageBox.information(
self,
translate('SongsPlugin.SongSelectForm', 'Free user'),
translate('SongsPlugin.SongSelectForm', 'You logged in with a free account, '
'the search will be limited to songs '
'in the public domain.')
)
if self.save_password_checkbox.isChecked():
self.settings.setValue('songs/songselect username', self.username_edit.text())
self.settings.setValue('songs/songselect password', self.password_edit.text())
else:
self.settings.remove('songs/songselect username')
self.settings.remove('songs/songselect password')
self.stacked_widget.setCurrentIndex(1)
self.login_progress_bar.setVisible(False)
self.login_progress_bar.setValue(0)
self.login_spacer.setVisible(True)
self.login_button.setEnabled(True)
self.username_edit.setEnabled(True)
self.password_edit.setEnabled(True)
self.save_password_checkbox.setEnabled(True)
self.search_combobox.setFocus()
self.application.process_events()
def on_search_button_clicked(self):
"""
Run a search on SongSelect.
"""
# Set up UI components
self.view_button.setEnabled(False)
self.search_button.setEnabled(False)
self.search_combobox.setEnabled(False)
self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(0)
self.search_progress_bar.setValue(0)
self.set_progress_visible(True)
self.search_results_widget.clear()
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
'Found {count:d} song(s)').format(count=self.song_count))
self.application.process_events()
self.song_count = 0
search_history = self.search_combobox.getItems()
self.settings.setValue('songs/songselect searches', '|'.join(search_history))
# Create thread and run search
worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText())
worker.show_info.connect(self.on_search_show_info)
worker.found_song.connect(self.on_search_found_song)
worker.finished.connect(self.on_search_finished)
run_thread(worker, 'songselect')
def on_stop_button_clicked(self):
"""
Stop the search when the stop button is clicked.
"""
self.song_select_importer.stop()
def on_search_show_info(self, title, message):
"""
Show an informational message from the search thread
:param title:
:param message:
"""
QtWidgets.QMessageBox.information(self, title, message)
def on_search_found_song(self, song):
"""
Add a song to the list when one is found.
:param song:
"""
self.song_count += 1
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm',
'Found {count:d} song(s)').format(count=self.song_count))
item_title = song['title']
if song['authors']:
item_title += ' (' + ', '.join(song['authors']) + ')'
song_item = QtWidgets.QListWidgetItem(item_title, self.search_results_widget)
song_item.setData(QtCore.Qt.UserRole, song)
def on_search_finished(self):
"""
Slot which is called when the search is completed.
"""
self.application.process_events()
self.set_progress_visible(False)
self.search_button.setEnabled(True)
self.search_combobox.setEnabled(True)
self.application.process_events()
def on_search_results_widget_selection_changed(self):
"""
Enable or disable the view button when the selection changes.
"""
self.view_button.setEnabled(len(self.search_results_widget.selectedItems()) > 0)
url = self.url_bar.text()
self.song_select_importer.set_page(url)
def on_view_button_clicked(self):
"""
View a song from SongSelect.
Import a song from SongSelect.
"""
self._view_song(self.search_results_widget.currentItem())
self.view_button.setEnabled(False)
self.url_bar.setEnabled(False)
self._view_song()
def on_search_results_widget_double_clicked(self, current_item):
def on_back_button_clicked(self, force_return_to_home=False):
"""
View a song from SongSelect
:param current_item:
Go back to the search page or just to the webview if on the preview screen
"""
self._view_song(current_item)
def on_back_button_clicked(self):
"""
Go back to the search page.
"""
self.stacked_widget.setCurrentIndex(1)
self.search_combobox.setFocus()
if (self.stacked_widget.currentIndex() == 0 or force_return_to_home):
self.song_select_importer.set_home_page()
else:
self.view_button.setEnabled(True)
self.url_bar.setEnabled(True)
self.stacked_widget.setCurrentIndex(0)
def on_import_button_clicked(self):
"""
Import a song from SongSelect.
"""
# Warn user if a song exists in the database with the same ccli_number
songs_with_same_ccli_number = self.plugin.manager.get_all_objects(
Song, and_(Song.ccli_number.like(self.song['ccli_number']), Song.ccli_number != ''))
if len(songs_with_same_ccli_number) > 0:
continue_import = QtWidgets.QMessageBox.question(self,
translate('SongsPlugin.SongSelectForm',
'Song Duplicate Warning'),
translate('SongsPlugin.SongSelectForm',
'A song with the same CCLI number is already in '
'your database.\n\n'
'Are you sure you want to import this song?'),
defaultButton=QtWidgets.QMessageBox.No)
if continue_import == QtWidgets.QMessageBox.No:
return
self.song_select_importer.save_song(self.song)
self.song = None
if QtWidgets.QMessageBox.question(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
QtWidgets.QMessageBox.information(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'),
translate('SongsPlugin.SongSelectForm',
'Your song has been imported, would you '
'like to import more songs?'),
defaultButton=QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
self.on_back_button_clicked()
else:
self.application.process_events()
self.done(QtWidgets.QDialog.Accepted)
def set_progress_visible(self, is_visible):
"""
Show or hide the search progress, including the stop button.
"""
self.search_progress_bar.setVisible(is_visible)
self.stop_button.setVisible(is_visible)
'Your song has been imported'))
self.on_back_button_clicked(True)

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2020 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Subclass of QWebEngineView used to block js messages; Useful for public sites
where we have no control on the code run in the webpage, and do not want to
see the error messages.
"""
from PyQt5 import QtWebEngineWidgets
class WebEnginePage(QtWebEngineWidgets.QWebEnginePage):
"""
A custom WebEngine page to capture Javascript console logging
"""
def javaScriptConsoleMessage(self, level, message, line_number, source_id):
"""
Override the parent method in order to hide the ccli site messages
"""
pass
class WebEngineView(QtWebEngineWidgets.QWebEngineView):
"""
A sub-classed QWebEngineView
"""
def __init__(self, parent=None):
"""
Constructor
"""
super(WebEngineView, self).__init__(parent)
self.setPage(WebEnginePage(self))

View File

@ -22,29 +22,18 @@
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
"""
import logging
import random
import re
from html import unescape
from html.parser import HTMLParser
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, URLError, build_opener
from urllib.request import URLError
from PyQt5 import QtCore
from bs4 import BeautifulSoup, NavigableString
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Song, Author, Topic
from openlp.plugins.songs.lib.openlyricsxml import SongXML
from openlp.core.common.utils import wait_for
USER_AGENTS = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/52.0.2743.116 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0',
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) Gecko/20100101 Firefox/47.0'
]
BASE_URL = 'https://songselect.ccli.com'
LOGIN_PAGE = 'https://profile.ccli.com/account/signin?appContext=SongSelect&returnUrl='\
'https%3a%2f%2fsongselect.ccli.com%2f'
@ -56,177 +45,140 @@ SONG_PAGE = BASE_URL + '/Songs/'
log = logging.getLogger(__name__)
class Pages(object):
"""
Songselect web page types.
"""
Login = 0
Home = 1
Search = 2
Song = 3
Other = 4
class SongSelectImport(object):
"""
The :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class contains all the code which interfaces
with CCLI's SongSelect service and downloads the songs.
"""
def __init__(self, db_manager):
def __init__(self, db_manager, webview):
"""
Set up the song select importer
:param db_manager: The song database manager
"""
self.db_manager = db_manager
self.html_parser = HTMLParser()
self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
self.opener.addheaders = [('User-Agent', random.choice(USER_AGENTS))]
self.run_search = True
self.webview = webview
def login(self, username, password, callback=None):
def get_page_type(self):
"""
Log the user into SongSelect. This method takes a username and password, and runs ``callback()`` at various
points which can be used to give the user some form of feedback.
Get the type of page the user is currently ono
:param username: SongSelect username
:param password: SongSelect password
:param callback: Method to notify of progress.
:return: subscription level on success, None on failure.
:return: The page the user is on
"""
if callback:
callback()
try:
login_page = BeautifulSoup(self.opener.open(LOGIN_PAGE).read(), 'lxml')
except (TypeError, URLError) as error:
log.exception('Could not login to SongSelect, {error}'.format(error=error))
return False
if callback:
callback()
token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'})
data = urlencode({
'__RequestVerificationToken': token_input['value'],
'emailAddress': username,
'password': password,
'RememberMe': 'false'
})
login_form = login_page.find('form')
if login_form:
login_url = login_form.attrs['action']
else:
login_url = '/Account/SignIn'
if not login_url.startswith('http'):
if login_url[0] != '/':
login_url = '/' + login_url
login_url = LOGIN_URL + login_url
try:
posted_page = BeautifulSoup(self.opener.open(login_url, data.encode('utf-8')).read(), 'lxml')
except (TypeError, URLError) as error:
log.exception('Could not login to SongSelect, {error}'.format(error=error))
return False
if callback:
callback()
# Page if user is in an organization
if posted_page.find('input', id='SearchText') is not None:
self.subscription_level = self.find_subscription_level(posted_page)
return self.subscription_level
# Page if user is not in an organization
elif posted_page.find('div', id="select-organization") is not None:
try:
home_page = BeautifulSoup(self.opener.open(BASE_URL).read(), 'lxml')
self.subscription_level = self.find_subscription_level(home_page)
except (TypeError, URLError) as error:
log.exception('Could not reach SongSelect, {error}'.format(error=error))
self.subscription_level = None
return self.subscription_level
else:
log.debug(posted_page)
return None
current_url_host = self.webview.page().url().host()
current_url_path = self.webview.page().url().path()
if (current_url_host == QtCore.QUrl(LOGIN_URL).host() and current_url_path == QtCore.QUrl(LOGIN_PAGE).path()):
return Pages.Login
elif (current_url_host == QtCore.QUrl(BASE_URL).host()):
if (current_url_path == '/' or current_url_path == ''):
return Pages.Home
elif (current_url_path == QtCore.QUrl(SEARCH_URL).path()):
return Pages.Search
elif (self.get_song_number_from_url(current_url_path) is not None):
return Pages.Song
return Pages.Other
def find_subscription_level(self, page):
scripts = page.find_all('script')
for tag in scripts:
if tag.string:
match = re.search("'Subscription': '(?P<subscription_level>[^']+)", tag.string)
if match:
return match.group('subscription_level')
log.error('Could not determine SongSelect subscription level')
def _run_javascript(self, script):
"""
Run a script and returns the result
:param script: The javascript to be run
:return: The evaluated result
"""
self.web_stuff = ""
self.got_web_stuff = False
def handle_result(result):
"""
Handle the result from the asynchronous call
"""
self.got_web_stuff = True
self.web_stuff = result
self.webview.page().runJavaScript(script, handle_result)
wait_for(lambda: self.got_web_stuff)
return self.web_stuff
def reset_webview(self):
"""
Sets the webview back to the login page using the Qt setUrl method
"""
url = QtCore.QUrl(LOGIN_PAGE)
self.webview.setUrl(url)
def set_home_page(self):
"""
Sets the webview to the search page
"""
self.set_page(BASE_URL)
def set_page(self, url):
"""
Sets the active page in the webview
:param url: The new page location
"""
script = 'document.location = "{}"'.format(url)
self._run_javascript(script)
def set_login_fields(self, username, password):
script_set_login_fields = ('document.getElementById("EmailAddress").value = "{email}";'
'document.getElementById("Password").value = "{password}";'
).format(email=username, password=password)
self._run_javascript(script_set_login_fields)
def get_page(self, url):
"""
Gets the html for the url through the active webview
:return: String containing a html document
"""
script_get_page = ('var openlp_page_data = null;'
'fetch("{}").then(data => {{return data.text()}})'
' .then(data => {{openlp_page_data = data}})').format(url)
self._run_javascript(script_get_page)
wait_for(lambda: self._run_javascript('openlp_page_data != null'))
return self._run_javascript('openlp_page_data')
def get_song_number_from_url(self, url):
"""
Gets the ccli song number for a song from the url
:return: String containg ccli song number, None is returned if not found
"""
ccli_number_regex = re.compile(r'.*?Songs\/([0-9]+).*', re.IGNORECASE)
regex_matches = ccli_number_regex.match(url)
if regex_matches:
return regex_matches.group(1)
return None
def logout(self):
"""
Log the user out of SongSelect
"""
try:
self.opener.open(LOGOUT_URL)
except (TypeError, URLError) as error:
log.exception('Could not log out of SongSelect, {error}'.format(error=error))
def search(self, search_text, max_results, callback=None):
"""
Set up a search.
:param search_text: The text to search for.
:param max_results: Maximum number of results to fetch.
:param callback: A method which is called when each song is found, with the song as a parameter.
:return: List of songs
"""
self.run_search = True
search_text = search_text.strip()
params = {
'SongContent': '',
'PrimaryLanguage': '',
'Keys': '',
'Themes': '',
'List': 'publicdomain' if self.subscription_level == 'Free' else '',
'Sort': '',
'SearchText': search_text
}
current_page = 1
songs = []
while self.run_search:
if current_page > 1:
params['CurrentPage'] = current_page
try:
results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + urlencode(params)).read(), 'lxml')
search_results = results_page.find_all('div', 'song-result')
except (TypeError, URLError) as error:
log.exception('Could not search SongSelect, {error}'.format(error=error))
results_page = None
search_results = None
if not search_results:
if results_page and re.compile('^[0-9]+$').match(search_text):
author_elements = results_page.find('ul', class_='authors').find_all('li')
song = {
'link': SONG_PAGE + search_text,
'authors': [unescape(li.find('a').string).strip() for li in author_elements],
'title': unescape(results_page.find('div', 'content-title').find('h1').string).strip()
}
if callback:
callback(song)
songs.append(song)
break
for result in search_results:
authors = result.find('p', 'song-result-subtitle').string
if authors:
authors = unescape(authors).strip().split(', ')
else:
authors = ""
song = {
'title': unescape(result.find('p', 'song-result-title').find('a').string).strip(),
'authors': authors,
'link': BASE_URL + result.find('p', 'song-result-title').find('a')['href']
}
if callback:
callback(song)
songs.append(song)
if len(songs) >= max_results:
self.run_search = False
break
current_page += 1
return songs
def get_song(self, song, callback=None):
def get_song(self, callback=None):
"""
Get the full song from SongSelect
:param song: The song dictionary to update
:param song: The song page url
:param callback: A callback which can be used to indicate progress
:return: The updated song dictionary
:return: Dictionary containing the song info
"""
song = {}
# Get current song
current_url = self.webview.url().path()
ccli_number = self.get_song_number_from_url(current_url)
if callback:
callback()
try:
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
song_page = BeautifulSoup(self.get_page(SONG_PAGE + ccli_number), 'lxml')
except (TypeError, URLError) as error:
log.exception('Could not get song from SongSelect, {error}'.format(error=error))
return None
@ -234,7 +186,7 @@ class SongSelectImport(object):
if callback:
callback()
try:
lyrics_page = BeautifulSoup(self.opener.open(BASE_URL + lyrics_link).read(), 'lxml')
lyrics_page = BeautifulSoup(self.get_page(BASE_URL + lyrics_link), 'lxml')
except (TypeError, URLError):
log.exception('Could not get lyrics from SongSelect')
return None
@ -249,6 +201,9 @@ class SongSelectImport(object):
copyright_elements.extend(ul.find_all('li')[1:])
if ul.find('li', string=themes_regex):
theme_elements.extend(ul.find_all('li')[1:])
author_elements = song_page.find('div', 'content-title').find('ul', 'authors').find_all('li')
song['title'] = unescape(song_page.find('div', 'content-title').find('h1').string.strip())
song['authors'] = [unescape(li.find('a').string).strip() for li in author_elements]
song['copyright'] = '/'.join([unescape(li.string).strip() for li in copyright_elements])
song['topics'] = [unescape(li.string).strip() for li in theme_elements]
song['ccli_number'] = song_page.find('div', 'song-content-data').find('ul').find('li')\
@ -273,7 +228,7 @@ class SongSelectImport(object):
"""
Save a song to the database, using the db_manager
:param song:
:param song: Dictionary of the song to save
:return:
"""
db_song = Song.populate(title=song['title'], copyright=song['copyright'], ccli_number=song['ccli_number'])
@ -312,9 +267,3 @@ class SongSelectImport(object):
db_song.topics.append(topic)
self.db_manager.save_object(db_song)
return db_song
def stop(self):
"""
Stop the search.
"""
self.run_search = False

View File

@ -79,6 +79,25 @@ 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)
# CCLI SongSelect login group box
self.ccli_login_group_box = QtWidgets.QGroupBox(self.left_column)
self.ccli_login_group_box.setObjectName('ccli_login_group_box')
self.ccli_login_layout = QtWidgets.QFormLayout(self.ccli_login_group_box)
self.ccli_login_layout.setObjectName('ccli_login_layout')
self.ccli_username_label = QtWidgets.QLabel(self.ccli_login_group_box)
self.ccli_username_label.setObjectName('ccli_username_label')
self.ccli_username = QtWidgets.QLineEdit(self.ccli_login_group_box)
self.ccli_username.setObjectName('ccli_username')
self.ccli_login_layout.addRow(self.ccli_username_label, self.ccli_username)
self.ccli_password_label = QtWidgets.QLabel(self.ccli_login_group_box)
self.ccli_password_label.setObjectName('ccli_password_label')
self.ccli_password = QtWidgets.QLineEdit(self.ccli_login_group_box)
self.ccli_password.setEchoMode(QtWidgets.QLineEdit.Password)
self.ccli_password.setObjectName('ccli_password')
self.ccli_login_layout.addRow(self.ccli_password_label, self.ccli_password)
self.left_layout.addWidget(self.ccli_login_group_box)
# Footer group box
self.footer_group_box = QtWidgets.QGroupBox(self.left_column)
self.footer_group_box.setObjectName('footer_group_box')
@ -121,6 +140,9 @@ class SongsTab(SettingsTab):
self.chords_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Chords'))
self.disable_chords_import_check_box.setText(translate('SongsPlugin.SongsTab',
'Ignore chords when importing songs'))
self.ccli_login_group_box.setTitle(translate('SongsPlugin.SongsTab', 'SongSelect Login'))
self.ccli_username_label.setText(translate('SongsPlugin.SongsTab', 'Username:'))
self.ccli_password_label.setText(translate('SongsPlugin.SongsTab', 'Password:'))
self.chord_notation_label.setText(translate('SongsPlugin.SongsTab', 'Chord notation to use:'))
self.english_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'English') + ' (C-D-E-F-G-A-B)')
self.german_notation_radio_button.setText(translate('SongsPlugin.SongsTab', 'German') + ' (C-D-E-F-G-A-H)')
@ -222,6 +244,8 @@ class SongsTab(SettingsTab):
self.neolatin_notation_radio_button.setChecked(True)
else:
self.english_notation_radio_button.setChecked(True)
self.ccli_username.setText(self.settings.value('songs/songselect username'))
self.ccli_password.setText(self.settings.value('songs/songselect password'))
self.footer_edit_box.setPlainText(self.settings.value('songs/footer template'))
def save(self):
@ -231,6 +255,19 @@ class SongsTab(SettingsTab):
self.settings.setValue('songs/enable chords', self.chords_group_box.isChecked())
self.settings.setValue('songs/disable chords import', self.disable_chords_import)
self.settings.setValue('songs/chord notation', self.chord_notation)
self.settings.setValue('songs/songselect username', self.ccli_username.text())
# Only save password if it's blank or the user acknowleges the warning
if (self.ccli_password.text() == ''):
self.settings.setValue('songs/songselect password', '')
elif (self.ccli_password.text() != self.settings.value('songs/songselect password')):
answer = QtWidgets.QMessageBox.question(
self, translate('SongsPlugin.SongsTab', 'Save Username and Password'),
translate('SongsPlugin.SongsTab', 'WARNING: Saving your SongSelect password is INSECURE, '
'your password is stored in PLAIN TEXT. Click Yes to save '
'your password or No to cancel this.'),
defaultButton=QtWidgets.QMessageBox.No)
if answer == QtWidgets.QMessageBox.Yes:
self.settings.setValue('songs/songselect password', self.ccli_password.text())
# Only save footer template if it has been changed. This allows future updates
if self.footer_edit_box.toPlainText() != self.settings.get_default_value('songs/footer template'):
self.settings.setValue('songs/footer template', self.footer_edit_box.toPlainText())

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ This module contains tests for the lib submodule of the Images plugin.
import pytest
from unittest.mock import MagicMock, patch
from PyQt5 import QtCore
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib.songstab import SongsTab
@ -135,6 +135,38 @@ def test_neolatin_notation_button(form):
assert form.chord_notation == 'neo-latin'
@patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question')
@patch('openlp.core.common.settings.Settings.setValue')
def test_password_change(mocked_settings_set_val, mocked_question, form):
"""
Test the ccli password sends a warning when changed, and saves when accepted
"""
# GIVEN: Warning is accepted and new password set
form.ccli_password.setText('new_password')
mocked_question.return_value = QtWidgets.QMessageBox.Yes
# WHEN: save is invoked
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once()
assert mocked_settings_set_val.call_count == 9
@patch('openlp.plugins.songs.lib.songstab.QtWidgets.QMessageBox.question')
@patch('openlp.core.common.settings.Settings.setValue')
def test_password_change_cancelled(mocked_settings_set_val, mocked_question, form):
"""
Test the ccli password sends a warning when changed and does not save when cancelled
"""
# GIVEN: Warning is not accepted and new password set
form.ccli_password.setText('new_password')
mocked_question.return_value = QtWidgets.QMessageBox.No
# WHEN: save is invoked
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
mocked_question.assert_called_once()
assert mocked_settings_set_val.call_count == 8
@patch('openlp.core.common.settings.Settings.setValue')
def test_footer_nochange(mocked_settings_set_val, form):
"""
@ -144,7 +176,7 @@ def test_footer_nochange(mocked_settings_set_val, form):
# WHEN: save is invoked
form.save()
# THEN: footer should not have been saved (one less call than the change test below)
assert mocked_settings_set_val.call_count == 7
assert mocked_settings_set_val.call_count == 9
@patch('openlp.core.common.settings.Settings.setValue')
@ -157,7 +189,7 @@ def test_footer_change(mocked_settings_set_val, form):
# WHEN: save is invoked
form.save()
# THEN: footer should have been saved (one more call to setValue than the nochange test)
assert mocked_settings_set_val.call_count == 8
assert mocked_settings_set_val.call_count == 10
assert form.footer_edit_box.toPlainText() == 'A new footer'