def search_returns_two_results_test(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds 2 results, it simply returns a list with 2 results """ # GIVEN: A bunch of mocked out stuff and an importer object # first search result mocked_result1 = MagicMock() mocked_result1.find.side_effect = [MagicMock(string='Title 1'), {'href': '/url1'}] mocked_result1.find_all.return_value = [MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2')] # second search result mocked_result2 = MagicMock() mocked_result2.find.side_effect = [MagicMock(string='Title 2'), {'href': '/url2'}] mocked_result2.find_all.return_value = [MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2')] # rest of the stuff mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2], []] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail 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 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_results_page.find_all.call_count, 'find_all should have been called twice') mocked_results_page.find_all.assert_called_with('li', 'result pane') expected_list = [ {'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1'}, {'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')
def test_login_url_from_form(self, MockedBeautifulSoup, mocked_build_opener): """ Test that the login URL is from the form """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_form = MagicMock() mocked_form.attrs = {'action': 'do/login'} mocked_login_page = MagicMock() mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form] mocked_posted_page = MagicMock() mocked_posted_page.find.return_value = MagicMock() mocked_home_page = MagicMock() MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page, mocked_home_page] mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned assert 3 == mock_callback.call_count, 'callback should have been called 3 times' assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page' assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page' assert 'https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0] assert result is None, 'The login method should have returned the subscription level'
def test_save_song_unknown_author(self, MockedAuthor, mocked_clean_song): """ Test that saving a song with an author name of only one word performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Unknown'], 'verses': [ {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = None importer = SongSelectImport(mocked_db_manager) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called self.assertIsInstance(result, Song, 'The returned value should be a Song object') mocked_clean_song.assert_called_with(mocked_db_manager, result) self.assertEqual(2, mocked_db_manager.save_object.call_count, 'The save_object() method should have been called twice') mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) MockedAuthor.populate.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown') self.assertEqual(1, len(result.authors_songs), 'There should only be one author')
def test_save_song_unknown_author(self, MockedAuthor, mocked_clean_song): """ Test that saving a song with an author name of only one word performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Unknown'], 'verses': [ {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'}, {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'}, {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'} ], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = None importer = SongSelectImport(mocked_db_manager) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called assert isinstance(result, Song), 'The returned value should be a Song object' mocked_clean_song.assert_called_with(mocked_db_manager, result) assert 2 == mocked_db_manager.save_object.call_count, \ 'The save_object() method should have been called twice' mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False) MockedAuthor.populate.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown') assert 1 == len(result.authors_songs), 'There should only be one author'
def test_search_returns_ccli_song_number_result(self, MockedBeautifulSoup, mocked_build_opener): """ Test that search can find a single song by CCLI number """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.return_value = [] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) importer.subscription_level = 'premium' # WHEN: The search is performed results = importer.search('1234567', 1000, mock_callback) # THEN: callback was called once and the results are as expected assert 1 == mock_callback.call_count, 'callback should not have been called' assert 1 == mocked_opener.open.call_count, 'open should have been called once' assert 1 == mocked_results_page.find_all.call_count, 'find_all should have been called once' mocked_results_page.find_all.assert_called_with('div', 'song-result') assert 1 == len(results), 'The search method should have returned an single song in a list' assert 'https://songselect.ccli.com/Songs/1234567' == results[0]['link'],\ 'The correct link should have been returned'
def test_search_returns_no_results(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds no results, it simply returns an empty list """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.return_value = [] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail 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 self.assertEqual(0, mock_callback.call_count, 'callback should not have been called') self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once') self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once') mocked_results_page.find_all.assert_called_with('div', 'song-result') self.assertEqual( [], results, 'The search method should have returned an empty list')
def initialise(self): """ Initialise the SongSelectForm """ self.thread = None self.worker = None 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.view_button.clicked.connect(self.on_view_button_clicked) self.back_button.clicked.connect(self.on_back_button_clicked) self.import_button.clicked.connect(self.on_import_button_clicked)
def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when logging in to SongSelect fails, the login method returns False """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_login_page = MagicMock() mocked_login_page.find.side_effect = [{'value': 'blah'}, None] mocked_posted_page = MagicMock() mocked_posted_page.find.return_value = None MockedBeautifulSoup.side_effect = [ mocked_login_page, mocked_posted_page ] mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned assert 3 == mock_callback.call_count, 'callback should have been called 3 times' assert 2 == mocked_login_page.find.call_count, 'find should have been called twice' assert 1 == mocked_posted_page.find.call_count, 'find should have been called once' assert 2 == mocked_opener.open.call_count, 'opener should have been called twice' assert result is False, 'The login method should have returned False'
def test_get_song(self, MockedBeautifulSoup, mocked_build_opener): """ Test that the get_song() method returns the correct song details """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_song_page = MagicMock() mocked_copyright = MagicMock() mocked_copyright.find_all.return_value = [ MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2') ] mocked_song_page.find.side_effect = [ mocked_copyright, MagicMock(find=MagicMock(string='CCLI: 123456')) ] mocked_lyrics_page = MagicMock() mocked_find_all = MagicMock() mocked_find_all.side_effect = [[ MagicMock( contents= 'The Lord told Noah: there\'s gonna be a floody, floody'), MagicMock( contents='So, rise and shine, and give God the glory, glory'), MagicMock(contents='The Lord told Noah to build him an arky, arky') ], [ MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2') ]] mocked_lyrics_page.find.return_value = MagicMock( find_all=mocked_find_all) MockedBeautifulSoup.side_effect = [ mocked_song_page, mocked_lyrics_page ] mocked_callback = MagicMock() importer = SongSelectImport(None) fake_song = { 'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url' } # WHEN: get_song is called result = importer.get_song(fake_song, callback=mocked_callback) # THEN: The callback should have been called three times and the song should be returned assert 3 == mocked_callback.call_count, 'The callback should have been called twice' assert result is not None, 'The get_song() method should have returned a song dictionary' assert 2 == mocked_lyrics_page.find.call_count, 'The find() method should have been called twice' assert 2 == mocked_find_all.call_count, 'The find_all() method should have been called twice' assert [call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')] == \ mocked_lyrics_page.find.call_args_list, 'The find() method should have been called with the right arguments' assert [call('p'), call('h3')] == mocked_find_all.call_args_list, \ 'The find_all() method should have been called with the right arguments' assert 'copyright' in result, 'The returned song should have a copyright' assert 'ccli_number' in result, 'The returned song should have a CCLI number' assert 'verses' in result, 'The returned song should have verses' assert 3 == len( result['verses']), 'Three verses should have been returned'
def test_search_reaches_max_results(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds MAX (2) results, it simply returns a list with those (2) """ # GIVEN: A bunch of mocked out stuff and an importer object # first search result mocked_result1 = MagicMock() mocked_result1.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock( string='Title 1'))), MagicMock(string='James, John'), MagicMock(find=MagicMock(return_value={'href': '/url1'})) ] # second search result mocked_result2 = MagicMock() mocked_result2.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock( string='Title 2'))), MagicMock(string='Philip'), MagicMock(find=MagicMock(return_value={'href': '/url2'})) ] # third search result mocked_result3 = MagicMock() mocked_result3.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock( string='Title 3'))), MagicMock(string='Luke, Matthew'), MagicMock(find=MagicMock(return_value={'href': '/url3'})) ] # rest of the stuff mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.side_effect = [[ mocked_result1, mocked_result2, mocked_result3 ], []] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The search method is called results = importer.search('text', 2, mock_callback) # THEN: callback was called twice, open was called twice, find_all was called twice, max results returned assert 2 == mock_callback.call_count, 'callback should have been called twice' assert 2 == mocked_opener.open.call_count, 'open should have been called twice' assert 2 == mocked_results_page.find_all.call_count, 'find_all should have been called twice' mocked_results_page.find_all.assert_called_with('div', 'song-result') expected_list = [{ 'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1' }, { 'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2' }] assert expected_list == results, 'The search method should have returned two songs'
def test_search_returns_two_results(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds 2 results, it simply returns a list with 2 results """ # GIVEN: A bunch of mocked out stuff and an importer object # first search result mocked_result1 = MagicMock() mocked_result1.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock( string='Title 1'))), MagicMock(string='James, John'), MagicMock(find=MagicMock(return_value={'href': '/url1'})) ] # second search result mocked_result2 = MagicMock() mocked_result2.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock( string='Title 2'))), MagicMock(string='Philip'), MagicMock(find=MagicMock(return_value={'href': '/url2'})) ] # rest of the stuff mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.side_effect = [[ mocked_result1, mocked_result2 ], []] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The search method is called 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 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_results_page.find_all.call_count, 'find_all should have been called twice') mocked_results_page.find_all.assert_called_with('div', 'song-result') expected_list = [{ 'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1' }, { 'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2' }] self.assertListEqual( expected_list, results, 'The search method should have returned two songs')
def test_stop_called(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')
def test_stop_called(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 importer.stop() # THEN: Searching should have stopped assert importer.run_search is False, 'Searching should have been stopped'
def save_song_existing_author_test(self, MockedAuthor, mocked_clean_song): """ Test that saving a song with an existing author performs the correct actions """ # GIVEN: A song to save, and some mocked out objects song_dict = { 'title': 'Arky Arky', 'authors': ['Public Domain'], 'verses': [{ 'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody' }, { 'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory' }, { 'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky' }], 'copyright': 'Public Domain', 'ccli_number': '123456' } MockedAuthor.display_name.__eq__.return_value = False mocked_db_manager = MagicMock() mocked_db_manager.get_object_filtered.return_value = MagicMock() importer = SongSelectImport(mocked_db_manager) # WHEN: The song is saved to the database result = importer.save_song(song_dict) # THEN: The return value should be a Song class and the mocked_db_manager should have been called self.assertIsInstance(result, Song, 'The returned value should be a Song object') mocked_clean_song.assert_called_with(mocked_db_manager, result) self.assertEqual( 2, mocked_db_manager.save_object.call_count, 'The save_object() method should have been called twice') mocked_db_manager.get_object_filtered.assert_called_with( MockedAuthor, False) self.assertEqual(0, MockedAuthor.populate.call_count, 'A new author should not have been instantiated') self.assertEqual(1, len(result.authors_songs), 'There should only be one author')
def test_login_except(self, mocked_build_opener): """ Test that when logging in to SongSelect fails, the login method raises URLError """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_build_opener.open.side_effect = URLError('Fake URLError') mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 1 time and False was returned self.assertEqual(1, mock_callback.call_count, 'callback should have been called 1 times') self.assertFalse(result, 'The login method should have returned False')
def test_login_except(self, mocked_build_opener): """ Test that when logging in to SongSelect fails, the login method raises URLError """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_build_opener.open.side_effect = URLError('Fake URLError') mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 1 time and False was returned assert 1 == mock_callback.call_count, 'callback should have been called 1 times' assert result is False, 'The login method should have returned False'
def test_logout(self, mocked_build_opener): """ Test that when the logout method is called, it logs the user out of SongSelect """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail importer.logout() # THEN: The opener is called once with the logout url self.assertEqual(1, mocked_opener.open.call_count, 'opener should have been called once') mocked_opener.open.assert_called_with(LOGOUT_URL)
def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')] mocked_callback = MagicMock() importer = SongSelectImport(None) # WHEN: get_song is called result = importer.get_song({'link': 'link'}, callback=mocked_callback) # THEN: The callback should have been called twice and None should be returned self.assertEqual(2, mocked_callback.call_count, 'The callback should have been called twice') self.assertIsNone(result, 'The get_song() method should have returned None')
def test_logout(self, mocked_build_opener): """ Test that when the logout method is called, it logs the user out of SongSelect """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail importer.logout() # THEN: The opener is called once with the logout url assert 1 == mocked_opener.open.call_count, 'opener should have been called once' mocked_opener.open.assert_called_with(LOGOUT_URL)
def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object song_page = MagicMock(return_value={'href': '/lyricpage'}) MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')] mocked_callback = MagicMock() importer = SongSelectImport(None) # WHEN: get_song is called result = importer.get_song({'link': 'link'}, callback=mocked_callback) # THEN: The callback should have been called twice and None should be returned assert 2 == mocked_callback.call_count, 'The callback should have been called twice' assert result is None, 'The get_song() method should have returned None'
def test_search_reaches_max_results(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds MAX (2) results, it simply returns a list with those (2) """ # GIVEN: A bunch of mocked out stuff and an importer object # first search result mocked_result1 = MagicMock() mocked_result1.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))), MagicMock(string='James, John'), MagicMock(find=MagicMock(return_value={'href': '/url1'})) ] # second search result mocked_result2 = MagicMock() mocked_result2.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))), MagicMock(string='Philip'), MagicMock(find=MagicMock(return_value={'href': '/url2'})) ] # third search result mocked_result3 = MagicMock() mocked_result3.find.side_effect = [ MagicMock(find=MagicMock(return_value=MagicMock(string='Title 3'))), MagicMock(string='Luke, Matthew'), MagicMock(find=MagicMock(return_value={'href': '/url3'})) ] # rest of the stuff mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2, mocked_result3], []] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The search method is called results = importer.search('text', 2, mock_callback) # 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, 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') mocked_results_page.find_all.assert_called_with('div', 'song-result') expected_list = [{'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'}, {'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}] self.assertListEqual(expected_list, results, 'The search method should have returned two songs')
def test_get_song_page_raises_exception(self, mocked_build_opener): """ Test that when BeautifulSoup gets a bad song page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known') mocked_callback = MagicMock() importer = SongSelectImport(None) # WHEN: get_song is called result = importer.get_song({'link': 'link'}, callback=mocked_callback) # THEN: The callback should have been called once and None should be returned mocked_callback.assert_called_with() assert result is None, 'The get_song() method should have returned None'
def test_get_song_page_raises_exception(self, mocked_build_opener): """ Test that when BeautifulSoup gets a bad song page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known') mocked_callback = MagicMock() importer = SongSelectImport(None) # WHEN: get_song is called result = importer.get_song({'link': 'link'}, callback=mocked_callback) # THEN: The callback should have been called once and None should be returned mocked_callback.assert_called_with() self.assertIsNone(result, 'The get_song() method should have returned None')
def test_get_song(self, MockedBeautifulSoup, mocked_build_opener): """ Test that the get_song() method returns the correct song details """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_song_page = MagicMock() mocked_copyright = MagicMock() mocked_copyright.find_all.return_value = [MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2')] mocked_song_page.find.side_effect = [ mocked_copyright, MagicMock(find=MagicMock(string='CCLI: 123456')) ] mocked_lyrics_page = MagicMock() mocked_find_all = MagicMock() mocked_find_all.side_effect = [ [ MagicMock(contents='The Lord told Noah: there\'s gonna be a floody, floody'), MagicMock(contents='So, rise and shine, and give God the glory, glory'), MagicMock(contents='The Lord told Noah to build him an arky, arky') ], [MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2')] ] mocked_lyrics_page.find.return_value = MagicMock(find_all=mocked_find_all) MockedBeautifulSoup.side_effect = [mocked_song_page, mocked_lyrics_page] mocked_callback = MagicMock() importer = SongSelectImport(None) fake_song = {'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url'} # WHEN: get_song is called result = importer.get_song(fake_song, callback=mocked_callback) # THEN: The callback should have been called three times and the song should be returned self.assertEqual(3, mocked_callback.call_count, 'The callback should have been called twice') self.assertIsNotNone(result, 'The get_song() method should have returned a song dictionary') self.assertEqual(2, mocked_lyrics_page.find.call_count, 'The find() method should have been called twice') self.assertEqual(2, mocked_find_all.call_count, 'The find_all() method should have been called twice') self.assertEqual([call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')], mocked_lyrics_page.find.call_args_list, 'The find() method should have been called with the right arguments') self.assertEqual([call('p'), call('h3')], mocked_find_all.call_args_list, 'The find_all() method should have been called with the right arguments') self.assertIn('copyright', result, 'The returned song should have a copyright') self.assertIn('ccli_number', result, 'The returned song should have a CCLI number') self.assertIn('verses', result, 'The returned song should have verses') self.assertEqual(3, len(result['verses']), 'Three verses should have been returned')
def get_song_lyrics_raise_exception_test(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None """ # GIVEN: A bunch of mocked out stuff and an importer object MockedBeautifulSoup.side_effect = [None, TypeError('Test Error')] mocked_callback = MagicMock() importer = SongSelectImport(None) # WHEN: get_song is called result = importer.get_song({'link': 'link'}, callback=mocked_callback) # THEN: The callback should have been called twice and None should be returned self.assertEqual(2, mocked_callback.call_count, 'The callback should have been called twice') self.assertIsNone(result, 'The get_song() method should have returned None')
def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when logging in to SongSelect fails, the login method returns False """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_login_page = MagicMock() mocked_login_page.find.side_effect = [{'value': 'blah'}, None] MockedBeautifulSoup.return_value = mocked_login_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 3 times, open was called twice, find was called twice, and False was returned self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times') self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice') self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice') self.assertFalse(result, 'The login method should have returned False')
def test_search_returns_no_results(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds no results, it simply returns an empty list """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.return_value = [] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail 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 self.assertEqual(0, mock_callback.call_count, 'callback should not have been called') self.assertEqual(1, mocked_opener.open.call_count, 'open should have been called once') self.assertEqual(1, mocked_results_page.find_all.call_count, 'find_all should have been called once') mocked_results_page.find_all.assert_called_with('div', 'song-result') self.assertEqual([], results, 'The search method should have returned an empty list')
def test_constructor(self, mocked_build_opener): """ Test that constructing a basic SongSelectImport object works correctly """ # GIVEN: The SongSelectImporter class and a mocked out build_opener # WHEN: An object is instantiated importer = SongSelectImport(None) # THEN: The object should have the correct properties assert importer.db_manager is None, 'The db_manager should be None' assert importer.html_parser is not None, 'There should be a valid html_parser object' assert importer.opener is not None, 'There should be a valid opener object' assert 1 == mocked_build_opener.call_count, 'The build_opener method should have been called once'
def test_login_succeeds(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when logging in to SongSelect succeeds, the login method returns True """ # GIVEN: A bunch of mocked out stuff and an importer object mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_login_page = MagicMock() mocked_login_page.find.side_effect = [{'value': 'blah'}, MagicMock()] MockedBeautifulSoup.return_value = mocked_login_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail result = importer.login('username', 'password', mock_callback) # THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned self.assertEqual(3, mock_callback.call_count, 'callback should have been called 3 times') self.assertEqual(2, mocked_login_page.find.call_count, 'find should have been called twice') self.assertEqual(2, mocked_opener.open.call_count, 'opener should have been called twice') self.assertTrue(result, 'The login method should have returned True')
def initialise(self): """ Initialise the SongSelectForm """ self.thread = None self.worker = None self.song_count = 0 self.song = None 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.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.view_button.clicked.connect(self.on_view_button_clicked) self.back_button.clicked.connect(self.on_back_button_clicked) self.import_button.clicked.connect(self.on_import_button_clicked)
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog): """ The :class:`SongSelectForm` class is the SongSelect dialog. """ def __init__(self, parent=None, plugin=None, db_manager=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint) self.plugin = plugin self.db_manager = db_manager self.setup_ui(self) def initialise(self): """ Initialise the SongSelectForm """ self.thread = None self.worker = None 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.view_button.clicked.connect(self.on_view_button_clicked) self.back_button.clicked.connect(self.on_back_button_clicked) self.import_button.clicked.connect(self.on_import_button_clicked) 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.view_button.setEnabled(False) if Settings().contains(self.plugin.settings_section + '/songselect password'): self.username_edit.setText(Settings().value(self.plugin.settings_section + '/songselect username')) self.password_edit.setText(Settings().value(self.plugin.settings_section + '/songselect password')) self.save_password_checkbox.setChecked(True) if Settings().contains(self.plugin.settings_section + '/songselect searches'): self.search_combobox.addItems( Settings().value(self.plugin.settings_section + '/songselect searches').split('|')) self.username_edit.setFocus() return QtWidgets.QDialog.exec(self) def done(self, r): """ Log out of SongSelect. :param r: 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, r) 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 _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.application.process_events() def _view_song(self, current_item): """ 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: 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 # 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.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.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() def on_save_password_checkbox_toggled(self, checked): """ Show a warning dialog when the user toggles the save checkbox on or off. :param checked: If the combobox is checked or not """ 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.'), QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), 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 if not self.song_select_importer.login( self.username_edit.text(), self.password_edit.text(), self._update_login_progress): 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 self.save_password_checkbox.isChecked(): Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text()) Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text()) else: Settings().remove(self.plugin.settings_section + '/songselect username') Settings().remove(self.plugin.settings_section + '/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() Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history)) # Create thread and run search self.thread = QtCore.QThread() self.worker = SearchWorker(self.song_select_importer, self.search_combobox.currentText()) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.start) self.worker.show_info.connect(self.on_search_show_info) self.worker.found_song.connect(self.on_search_found_song) self.worker.finished.connect(self.on_search_finished) self.worker.quit.connect(self.thread.quit) self.worker.quit.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) 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): """ 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'] + ' (' + ', '.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) def on_view_button_clicked(self): """ View a song from SongSelect. """ self._view_song(self.search_results_widget.currentItem()) def on_search_results_widget_double_clicked(self, current_item): """ View a song from SongSelect :param current_item: """ 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() def on_import_button_clicked(self): """ Import a song from SongSelect. """ self.song_select_importer.save_song(self.song) self.song = None if QtWidgets.QMessageBox.question(self, translate('SongsPlugin.SongSelectForm', 'Song Imported'), translate('SongsPlugin.SongSelectForm', 'Your song has been imported, would you ' 'like to import more songs?'), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, 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) @property def application(self): """ Adds the openlp to the class dynamically. Windows needs to access the application in a dynamic manner. """ if is_win(): return Registry().get('application') else: if not hasattr(self, '_application'): self._application = Registry().get('application') return self._application
def search_reaches_max_results_test(self, MockedBeautifulSoup, mocked_build_opener): """ Test that when the search finds MAX (2) results, it simply returns a list with those (2) """ # GIVEN: A bunch of mocked out stuff and an importer object # first search result mocked_result1 = MagicMock() mocked_result1.find.side_effect = [ MagicMock(string='Title 1'), { 'href': '/url1' } ] mocked_result1.find_all.return_value = [ MagicMock(string='Author 1-1'), MagicMock(string='Author 1-2') ] # second search result mocked_result2 = MagicMock() mocked_result2.find.side_effect = [ MagicMock(string='Title 2'), { 'href': '/url2' } ] mocked_result2.find_all.return_value = [ MagicMock(string='Author 2-1'), MagicMock(string='Author 2-2') ] # third search result mocked_result3 = MagicMock() mocked_result3.find.side_effect = [ MagicMock(string='Title 3'), { 'href': '/url3' } ] mocked_result3.find_all.return_value = [ MagicMock(string='Author 3-1'), MagicMock(string='Author 3-2') ] # rest of the stuff mocked_opener = MagicMock() mocked_build_opener.return_value = mocked_opener mocked_results_page = MagicMock() mocked_results_page.find_all.side_effect = [[ mocked_result1, mocked_result2, mocked_result3 ], []] MockedBeautifulSoup.return_value = mocked_results_page mock_callback = MagicMock() importer = SongSelectImport(None) # WHEN: The login method is called after being rigged to fail 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 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_results_page.find_all.call_count, 'find_all should have been called twice') mocked_results_page.find_all.assert_called_with('li', 'result pane') expected_list = [{ 'title': 'Title 1', 'authors': ['Author 1-1', 'Author 1-2'], 'link': BASE_URL + '/url1' }, { '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')
class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties): """ The :class:`SongSelectForm` class is the SongSelect dialog. """ def __init__(self, parent=None, plugin=None, db_manager=None): QtWidgets.QDialog.__init__( self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowCloseButtonHint) self.plugin = plugin self.db_manager = db_manager self.setup_ui(self) def initialise(self): """ 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.view_button.clicked.connect(self.on_view_button_clicked) self.back_button.clicked.connect(self.on_back_button_clicked) self.import_button.clicked.connect(self.on_import_button_clicked) 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.view_button.setEnabled(False) if self.settings.contains(self.plugin.settings_section + '/songselect password'): self.username_edit.setText( self.settings.value(self.plugin.settings_section + '/songselect username')) self.password_edit.setText( self.settings.value(self.plugin.settings_section + '/songselect password')) self.save_password_checkbox.setChecked(True) if self.settings.contains(self.plugin.settings_section + '/songselect searches'): self.search_combobox.addItems( self.settings.value(self.plugin.settings_section + '/songselect searches').split('|')) self.username_edit.setFocus() return QtWidgets.QDialog.exec(self) def done(self, result_code): """ Log out of SongSelect. :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 _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.application.process_events() def _view_song(self, current_item): """ 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: 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 # 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.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.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() def on_save_password_checkbox_toggled(self, checked): """ Show a warning dialog when the user toggles the save checkbox on or off. :param checked: If the combobox is checked or not """ 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( self.plugin.settings_section + '/songselect username', self.username_edit.text()) self.settings.setValue( self.plugin.settings_section + '/songselect password', self.password_edit.text()) else: self.settings.remove(self.plugin.settings_section + '/songselect username') self.settings.remove(self.plugin.settings_section + '/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( self.plugin.settings_section + '/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'] + ' (' + ', '.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) def on_view_button_clicked(self): """ View a song from SongSelect. """ self._view_song(self.search_results_widget.currentItem()) def on_search_results_widget_double_clicked(self, current_item): """ View a song from SongSelect :param current_item: """ 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() def on_import_button_clicked(self): """ Import a song from SongSelect. """ self.song_select_importer.save_song(self.song) self.song = None if QtWidgets.QMessageBox.question( 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)