[Song select] Stop search on viewing a song.

[Song select] Add a stop button to the SongSelect importer to stop searching.
[Song select] Fix a potential bug where the song author only has 1 name (most commonly seen when the name is 'Unknown')

Add this to your merge proposal:
--------------------------------
lp:~raoul-snyman/openlp/song-select-fixes (revision 2582)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1227/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1152/
[SUCC...

bzr-revno: 2598
This commit is contained in:
raoul@snyman.info 2016-01-09 19:38:10 +00:00 committed by Tim Bentley
commit d5856f02c6
6 changed files with 85 additions and 8 deletions

View File

@ -114,12 +114,19 @@ class Ui_SongSelectDialog(object):
self.search_button.setObjectName('search_button') self.search_button.setObjectName('search_button')
self.search_input_layout.addWidget(self.search_button) self.search_input_layout.addWidget(self.search_button)
self.search_layout.addLayout(self.search_input_layout) 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 = QtWidgets.QProgressBar(self.search_page)
self.search_progress_bar.setMinimum(0) self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(3) self.search_progress_bar.setMaximum(3)
self.search_progress_bar.setValue(0) self.search_progress_bar.setValue(0)
self.search_progress_bar.setVisible(False) self.search_progress_layout.addWidget(self.search_progress_bar)
self.search_layout.addWidget(self.search_progress_bar) self.stop_button = QtWidgets.QPushButton(self.search_page)
self.stop_button.setIcon(build_icon(':/songs/song_search_stop.png'))
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 = QtWidgets.QListWidget(self.search_page)
self.search_results_widget.setProperty("showDropIndicator", False) self.search_results_widget.setProperty("showDropIndicator", False)
self.search_results_widget.setAlternatingRowColors(True) self.search_results_widget.setAlternatingRowColors(True)
@ -234,6 +241,7 @@ class Ui_SongSelectDialog(object):
self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login')) self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login'))
self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:')) self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:'))
self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search')) 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 %s song(s)') % 0) self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % 0)
self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout')) self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout'))
self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View')) self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View'))

View File

@ -95,11 +95,13 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
self.worker = None self.worker = None
self.song_count = 0 self.song_count = 0
self.song = None self.song = None
self.set_progress_visible(False)
self.song_select_importer = SongSelectImport(self.db_manager) self.song_select_importer = SongSelectImport(self.db_manager)
self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled) self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled)
self.login_button.clicked.connect(self.on_login_button_clicked) self.login_button.clicked.connect(self.on_login_button_clicked)
self.search_button.clicked.connect(self.on_search_button_clicked) self.search_button.clicked.connect(self.on_search_button_clicked)
self.search_combobox.returnPressed.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.logout_button.clicked.connect(self.done)
self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked) 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.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed)
@ -154,18 +156,30 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
return QtWidgets.QDialog.done(self, r) return QtWidgets.QDialog.done(self, r)
def _update_login_progress(self): 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.login_progress_bar.setValue(self.login_progress_bar.value() + 1)
self.application.process_events() self.application.process_events()
def _update_song_progress(self): def _update_song_progress(self):
"""
Update the progress bar as the song is being downloaded.
"""
self.song_progress_bar.setValue(self.song_progress_bar.value() + 1) self.song_progress_bar.setValue(self.song_progress_bar.value() + 1)
self.application.process_events() self.application.process_events()
def _view_song(self, current_item): def _view_song(self, current_item):
"""
Load a song into the song view.
"""
if not current_item: if not current_item:
return return
else: else:
current_item = current_item.data(QtCore.Qt.UserRole) 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.song_progress_bar.setVisible(True)
self.import_button.setEnabled(False) self.import_button.setEnabled(False)
self.back_button.setEnabled(False) self.back_button.setEnabled(False)
@ -289,7 +303,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
self.search_progress_bar.setMinimum(0) self.search_progress_bar.setMinimum(0)
self.search_progress_bar.setMaximum(0) self.search_progress_bar.setMaximum(0)
self.search_progress_bar.setValue(0) self.search_progress_bar.setValue(0)
self.search_progress_bar.setVisible(True) self.set_progress_visible(True)
self.search_results_widget.clear() self.search_results_widget.clear()
self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count) self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count)
self.application.process_events() self.application.process_events()
@ -309,6 +323,12 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
self.thread.finished.connect(self.thread.deleteLater) self.thread.finished.connect(self.thread.deleteLater)
self.thread.start() self.thread.start()
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): def on_search_show_info(self, title, message):
""" """
Show an informational message from the search thread Show an informational message from the search thread
@ -333,7 +353,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
Slot which is called when the search is completed. Slot which is called when the search is completed.
""" """
self.application.process_events() self.application.process_events()
self.search_progress_bar.setVisible(False) self.set_progress_visible(False)
self.search_button.setEnabled(True) self.search_button.setEnabled(True)
self.application.process_events() self.application.process_events()
@ -381,6 +401,13 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
self.application.process_events() self.application.process_events()
self.done(QtWidgets.QDialog.Accepted) 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)
@property @property
def application(self): def application(self):
""" """

View File

@ -61,6 +61,7 @@ class SongSelectImport(object):
self.html_parser = HTMLParser() self.html_parser = HTMLParser()
self.opener = build_opener(HTTPCookieProcessor(CookieJar())) self.opener = build_opener(HTTPCookieProcessor(CookieJar()))
self.opener.addheaders = [('User-Agent', USER_AGENT)] self.opener.addheaders = [('User-Agent', USER_AGENT)]
self.run_search = True
def login(self, username, password, callback=None): def login(self, username, password, callback=None):
""" """
@ -115,10 +116,11 @@ class SongSelectImport(object):
:param callback: A method which is called when each song is found, with the song as a parameter. :param callback: A method which is called when each song is found, with the song as a parameter.
:return: List of songs :return: List of songs
""" """
self.run_search = True
params = {'allowredirect': 'false', 'SearchTerm': search_text} params = {'allowredirect': 'false', 'SearchTerm': search_text}
current_page = 1 current_page = 1
songs = [] songs = []
while True: while self.run_search:
if current_page > 1: if current_page > 1:
params['page'] = current_page params['page'] = current_page
try: try:
@ -220,3 +222,9 @@ class SongSelectImport(object):
db_song.add_author(author) db_song.add_author(author)
self.db_manager.save_object(db_song) self.db_manager.save_object(db_song)
return db_song return db_song
def stop(self):
"""
Stop the search.
"""
self.run_search = False

View File

@ -1,5 +1,6 @@
<RCC> <RCC>
<qresource prefix="songs"> <qresource prefix="songs">
<file>song_search_stop.png</file>
<file>song_search_all.png</file> <file>song_search_all.png</file>
<file>song_search_author.png</file> <file>song_search_author.png</file>
<file>song_search_lyrics.png</file> <file>song_search_lyrics.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

View File

@ -192,7 +192,7 @@ class TestSongSelectImport(TestCase, TestMixin):
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail # WHEN: The search method is called
results = importer.search('text', 1000, mock_callback) results = importer.search('text', 1000, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned # THEN: callback was never called, open was called once, find_all was called once, an empty list returned
@ -234,10 +234,10 @@ class TestSongSelectImport(TestCase, TestMixin):
mock_callback = MagicMock() mock_callback = MagicMock()
importer = SongSelectImport(None) importer = SongSelectImport(None)
# WHEN: The login method is called after being rigged to fail # WHEN: The search method is called
results = importer.search('text', 2, mock_callback) results = importer.search('text', 2, mock_callback)
# THEN: callback was never called, open was called once, find_all was called once, an empty list returned # THEN: callback was called twice, open was called twice, find_all was called twice, max results returned
self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice') self.assertEqual(2, mock_callback.call_count, 'callback should have been called twice')
self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice') self.assertEqual(2, mocked_opener.open.call_count, 'open should have been called twice')
self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice') self.assertEqual(2, mocked_results_page.find_all.call_count, 'find_all should have been called twice')
@ -246,6 +246,22 @@ class TestSongSelectImport(TestCase, TestMixin):
{'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}] {'title': 'Title 2', 'authors': ['Author 2-1', 'Author 2-2'], 'link': BASE_URL + '/url2'}]
self.assertListEqual(expected_list, results, 'The search method should have returned two songs') self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
@patch('openlp.plugins.songs.lib.songselect.build_opener')
@patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
def stop_called_test(self, MockedBeautifulSoup, mocked_build_opener):
"""
Test that the search is stopped with stop() is called
"""
# GIVEN: An importer object that is currently "searching"
importer = SongSelectImport(None)
importer.run_search = True
# WHEN: The stop method is called
results = importer.stop()
# THEN: Searching should have stopped
self.assertFalse(importer.run_search, 'Searching should have been stopped')
@patch('openlp.plugins.songs.lib.songselect.build_opener') @patch('openlp.plugins.songs.lib.songselect.build_opener')
def get_song_page_raises_exception_test(self, mocked_build_opener): def get_song_page_raises_exception_test(self, mocked_build_opener):
""" """
@ -686,6 +702,23 @@ class TestSongSelectForm(TestCase, TestMixin):
# THEN: The view button should be enabled # THEN: The view button should be enabled
mocked_view_button.setEnabled.assert_called_with(True) mocked_view_button.setEnabled.assert_called_with(True)
@patch('openlp.plugins.songs.forms.songselectform.SongSelectImport')
def on_stop_button_clicked_test(self, MockedSongSelectImport):
"""
Test that the search is stopped when the stop button is clicked
"""
# GIVEN: A mocked SongSelectImporter and a SongSelect form
mocked_song_select_importer = MagicMock()
MockedSongSelectImport.return_value = mocked_song_select_importer
ssform = SongSelectForm(None, MagicMock(), MagicMock())
ssform.initialise()
# WHEN: The stop button is clicked
ssform.on_stop_button_clicked()
# THEN: The view button should be enabled
mocked_song_select_importer.stop.assert_called_with()
class TestSongSelectFileImport(SongImportTestHelper): class TestSongSelectFileImport(SongImportTestHelper):