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')
Exemple #2
0
    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'
Exemple #3
0
    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')
Exemple #4
0
    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'
Exemple #5
0
    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'
Exemple #6
0
    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')
Exemple #7
0
 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)
Exemple #8
0
    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'
Exemple #9
0
    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'
Exemple #10
0
    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'
Exemple #11
0
    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')
Exemple #12
0
    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')
Exemple #13
0
    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'
Exemple #14
0
    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')
Exemple #15
0
    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')
Exemple #16
0
    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'
Exemple #17
0
    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)
Exemple #18
0
    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')
Exemple #19
0
    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)
Exemple #20
0
    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'
Exemple #21
0
    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')
Exemple #22
0
    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'
Exemple #23
0
    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')
Exemple #24
0
    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')
Exemple #25
0
    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')
Exemple #26
0
    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')
Exemple #27
0
    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')
Exemple #28
0
    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'
Exemple #29
0
    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)
Exemple #31
0
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
Exemple #32
0
    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')
Exemple #33
0
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)