Exemple #1
0
 def initialise(self):
     """
     Initialise variables when they cannot be initialised in the constructor.
     """
     self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
     self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
     self.open_lyrics = OpenLyrics(self.plugin.manager)
     self.search_text_edit.set_search_types([
         (SongSearch.Entire, UiIcons().music,
             translate('SongsPlugin.MediaItem', 'Entire Song'),
             translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
         (SongSearch.Titles, UiIcons().search_text,
             translate('SongsPlugin.MediaItem', 'Titles'),
             translate('SongsPlugin.MediaItem', 'Search Titles...')),
         (SongSearch.Lyrics, UiIcons().search_lyrcs,
             translate('SongsPlugin.MediaItem', 'Lyrics'),
             translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
         (SongSearch.Authors, UiIcons().user, SongStrings.Authors,
             translate('SongsPlugin.MediaItem', 'Search Authors...')),
         (SongSearch.Topics, UiIcons().theme, SongStrings.Topics,
             translate('SongsPlugin.MediaItem', 'Search Topics...')),
         (SongSearch.Books, UiIcons().address, SongStrings.SongBooks,
             translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
         (SongSearch.Themes, UiIcons().theme, UiStrings().Themes, UiStrings().SearchThemes),
         (SongSearch.Copyright, UiIcons().copyright,
             translate('SongsPlugin.MediaItem', 'Copyright'),
             translate('SongsPlugin.MediaItem', 'Search Copyright...')),
         (SongSearch.CCLInumber, UiIcons().search_ccli,
             translate('SongsPlugin.MediaItem', 'CCLI number'),
             translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
     ])
     self.config_update()
Exemple #2
0
 def initialise(self):
     """
     Initialise variables when they cannot be initialised in the constructor.
     """
     self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
     self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
     self.open_lyrics = OpenLyrics(self.plugin.manager)
     self.search_text_edit.set_search_types([
         (SongSearch.Entire, ':/songs/song_search_all.png',
             translate('SongsPlugin.MediaItem', 'Entire Song'),
             translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
         (SongSearch.Titles, ':/songs/song_search_title.png',
             translate('SongsPlugin.MediaItem', 'Titles'),
             translate('SongsPlugin.MediaItem', 'Search Titles...')),
         (SongSearch.Lyrics, ':/songs/song_search_lyrics.png',
             translate('SongsPlugin.MediaItem', 'Lyrics'),
             translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
         (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
             translate('SongsPlugin.MediaItem', 'Search Authors...')),
         (SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics,
             translate('SongsPlugin.MediaItem', 'Search Topics...')),
         (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
             translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
         (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes),
         (SongSearch.Copyright, ':/songs/song_search_copy.png',
             translate('SongsPlugin.MediaItem', 'Copyright'),
             translate('SongsPlugin.MediaItem', 'Search Copyright...')),
         (SongSearch.CCLInumber, ':/songs/song_search_ccli.png',
             translate('SongsPlugin.MediaItem', 'CCLI number'),
             translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
     ])
     self.search_text_edit.set_current_search_type(
         Settings().value('{section}/last search type'.format(section=self.settings_section)))
     self.config_update()
Exemple #3
0
 def setUp(self):
     """
     Create the UI
     """
     Registry.create()
     self.setup_application()
     self.main_window = QtWidgets.QMainWindow()
     Registry().register('main_window', self.main_window)
     Registry().register('theme_manager', MagicMock())
     self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())
Exemple #4
0
 def setUp(self):
     """
     Set up the components need for all tests.
     """
     Registry.create()
     Registry().register('service_list', MagicMock())
     Registry().register('main_window', MagicMock())
     with patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__', return_value=None):
         self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
     self.setup_application()
     self.build_settings()
     QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
Exemple #5
0
 def setUp(self):
     """
     Create the UI
     """
     Registry.create()
     self.setup_application()
     self.main_window = QtWidgets.QMainWindow()
     Registry().register('main_window', self.main_window)
     Registry().register('theme_manager', MagicMock())
     self.build_settings()
     Settings().extend_default_settings(__default_settings__)
     Registry().register('settings', Settings())
     self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())
Exemple #6
0
 def initialise(self):
     """
     Initialise variables when they cannot be initialised in the constructor.
     """
     self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
     self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
     self.open_lyrics = OpenLyrics(self.plugin.manager)
     self.search_text_edit.set_search_types([
         (SongSearch.Entire, ':/songs/song_search_all.png',
             translate('SongsPlugin.MediaItem', 'Entire Song'),
             translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
         (SongSearch.Titles, ':/songs/song_search_title.png',
             translate('SongsPlugin.MediaItem', 'Titles'),
             translate('SongsPlugin.MediaItem', 'Search Titles...')),
         (SongSearch.Lyrics, ':/songs/song_search_lyrics.png',
             translate('SongsPlugin.MediaItem', 'Lyrics'),
             translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
         (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
             translate('SongsPlugin.MediaItem', 'Search Authors...')),
         (SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics,
             translate('SongsPlugin.MediaItem', 'Search Topics...')),
         (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
             translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
         (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes),
         (SongSearch.Copyright, ':/songs/song_search_copy.png',
             translate('SongsPlugin.MediaItem', 'Copyright'),
             translate('SongsPlugin.MediaItem', 'Search Copyright...')),
         (SongSearch.CCLInumber, ':/songs/song_search_ccli.png',
             translate('SongsPlugin.MediaItem', 'CCLI number'),
             translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
     ])
     self.config_update()
Exemple #7
0
 def initialise(self):
     """
     Initialise variables when they cannot be initialised in the constructor.
     """
     self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
     self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
     self.open_lyrics = OpenLyrics(self.plugin.manager)
     self.search_text_edit.set_search_types([
         (SongSearch.Entire, ':/songs/song_search_all.png',
             translate('SongsPlugin.MediaItem', 'Entire Song'),
             translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
         (SongSearch.Titles, ':/songs/song_search_title.png',
             translate('SongsPlugin.MediaItem', 'Titles'),
             translate('SongsPlugin.MediaItem', 'Search Titles...')),
         (SongSearch.Lyrics, ':/songs/song_search_lyrics.png',
             translate('SongsPlugin.MediaItem', 'Lyrics'),
             translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
         (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
             translate('SongsPlugin.MediaItem', 'Search Authors...')),
         (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
             translate('SongsPlugin.MediaItem', 'Search Song Books...')),
         (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes)
     ])
     self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
     self.config_update()
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the functions in the :mod:`lib` module.
    """
    def setUp(self):
        """
        Set up the components need for all tests.
        """
        Registry.create()
        Registry().register('service_list', MagicMock())
        Registry().register('main_window', MagicMock())
        with patch(
                'openlp.plugins.songs.forms.editsongform.EditSongForm.__init__',
                return_value=None):
            self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
        self.setup_application()
        self.build_settings()
        QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.destroy_settings()

    def test_validate_matching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")

    def test_validate_nonmatching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{br}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")
Exemple #9
0
 def setUp(self):
     """
     Create the UI
     """
     Registry.create()
     self.setup_application()
     self.main_window = QtWidgets.QMainWindow()
     Registry().register('main_window', self.main_window)
     Registry().register('theme_manager', MagicMock())
     self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the functions in the :mod:`lib` module.
    """
    def setUp(self):
        """
        Set up the components need for all tests.
        """
        Registry.create()
        Registry().register('service_list', MagicMock())
        Registry().register('main_window', MagicMock())
        with patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__', return_value=None):
            self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
        self.setup_application()
        self.build_settings()
        QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.destroy_settings()

    def validate_matching_tags_test(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")

    def validate_nonmatching_tags_test(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{br}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")
Exemple #11
0
 def setUp(self):
     """
     Set up the components need for all tests.
     """
     Registry.create()
     Registry().register('service_list', MagicMock())
     Registry().register('main_window', MagicMock())
     with patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__', return_value=None):
         self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
     self.setup_application()
     self.build_settings()
     QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
Exemple #12
0
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the EditSongForm class
    """
    def setUp(self):
        """
        Create the UI
        """
        Registry.create()
        self.setup_application()
        self.main_window = QtWidgets.QMainWindow()
        Registry().register('main_window', self.main_window)
        Registry().register('theme_manager', MagicMock())
        self.build_settings()
        Settings().extend_default_settings(__default_settings__)
        self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.destroy_settings()
        del self.form
        del self.main_window

    def test_ui_defaults(self):
        """
        Test that the EditSongForm defaults are correct
        """
        assert self.form.verse_edit_button.isEnabled(
        ) is False, 'The verse edit button should not be enabled'
        assert self.form.verse_delete_button.isEnabled(
        ) is False, 'The verse delete button should not be enabled'
        assert self.form.author_remove_button.isEnabled(
        ) is False, 'The author remove button should not be enabled'
        assert self.form.topic_remove_button.isEnabled(
        ) is False, 'The topic remove button should not be enabled'

    def test_is_verse_edit_form_executed(self):
        pass

    def test_verse_order_no_warning(self):
        """
        Test if the verse order warning is not shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1 V2'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(
            side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(
            return_value=given_verse_order.split())

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: No text should be shown.
        assert self.form.warning_label.text(
        ) == '', 'There should be no warning.'

    def test_verse_order_incomplete_warning(self):
        """
        Test if the verse-order-incomple warning is shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(
            side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(
            return_value=[given_verse_order])

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The verse-order-incomplete text should be shown.
        assert self.form.warning_label.text() == self.form.not_all_verses_used_warning, \
            'The verse-order-incomplete warning should be shown.'

    def test_bug_1170435(self):
        """
        Regression test for bug 1170435 (test if "no verse order" message is shown)
        """
        # GIVEN: Mocked methods.
        given_verse_order = ''
        self.form.verse_list_widget.rowCount = MagicMock(return_value=1)
        # Mock out the verse. (We want a verse type to be returned).
        mocked_verse = MagicMock()
        mocked_verse.data.return_value = 'V1'
        self.form.verse_list_widget.item = MagicMock(return_value=mocked_verse)
        self.form._extract_verse_order = MagicMock(return_value=[])
        self.form.verse_order_edit.text = MagicMock(
            return_value=given_verse_order)
        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The no-verse-order message should be shown.
        assert self.form.warning_label.text() == self.form.no_verse_order_entered_warning,  \
            'The no-verse-order message should be shown.'

    def test_bug_1404967(self):
        """
        Test for CCLI label showing correct text
        """
        # GIVEN; Mocked methods
        form = self.form
        # THEN: CCLI label should be CCLI song label
        assert form.ccli_label.text() is not UiStrings().CCLINumberLabel, \
            'CCLI label should not be "{}"'.format(UiStrings().CCLINumberLabel)
        assert form.ccli_label.text() == UiStrings().CCLISongNumberLabel, \
            'CCLI label text should be "{}"'.format(UiStrings().CCLISongNumberLabel)

    def test_verse_order_lowercase(self):
        """
        Test that entering a verse order in lowercase automatically converts to uppercase
        """
        # GIVEN; Mocked methods
        form = self.form

        # WHEN: We enter a verse order in lowercase
        form.verse_order_edit.setText('v1 v2 c1 v3 c1 v4 c1')
        # Need to manually trigger this method as it is only triggered by manual input
        form.on_verse_order_text_changed(form.verse_order_edit.text())

        # THEN: The verse order should be converted to uppercase
        assert form.verse_order_edit.text() == 'V1 V2 C1 V3 C1 V4 C1'
Exemple #13
0
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the EditSongForm class
    """

    def setUp(self):
        """
        Create the UI
        """
        Registry.create()
        self.setup_application()
        self.main_window = QtWidgets.QMainWindow()
        Registry().register('main_window', self.main_window)
        Registry().register('theme_manager', MagicMock())
        self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        del self.form
        del self.main_window

    def test_ui_defaults(self):
        """
        Test that the EditSongForm defaults are correct
        """
        self.assertFalse(self.form.verse_edit_button.isEnabled(), 'The verse edit button should not be enabled')
        self.assertFalse(self.form.verse_delete_button.isEnabled(), 'The verse delete button should not be enabled')
        self.assertFalse(self.form.author_remove_button.isEnabled(), 'The author remove button should not be enabled')
        self.assertFalse(self.form.topic_remove_button.isEnabled(), 'The topic remove button should not be enabled')

    def test_is_verse_edit_form_executed(self):
        pass

    def test_verse_order_no_warning(self):
        """
        Test if the verse order warning is not shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1 V2'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(return_value=given_verse_order.split())

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: No text should be shown.
        assert self.form.warning_label.text() == '', 'There should be no warning.'

    def test_verse_order_incomplete_warning(self):
        """
        Test if the verse-order-incomple warning is shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(return_value=[given_verse_order])

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The verse-order-incomplete text should be shown.
        assert self.form.warning_label.text() == self.form.not_all_verses_used_warning, \
            'The verse-order-incomplete warning should be shown.'

    def test_bug_1170435(self):
        """
        Regression test for bug 1170435 (test if "no verse order" message is shown)
        """
        # GIVEN: Mocked methods.
        given_verse_order = ''
        self.form.verse_list_widget.rowCount = MagicMock(return_value=1)
        # Mock out the verse. (We want a verse type to be returned).
        mocked_verse = MagicMock()
        mocked_verse.data.return_value = 'V1'
        self.form.verse_list_widget.item = MagicMock(return_value=mocked_verse)
        self.form._extract_verse_order = MagicMock(return_value=[])
        self.form.verse_order_edit.text = MagicMock(return_value=given_verse_order)
        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The no-verse-order message should be shown.
        assert self.form.warning_label.text() == self.form.no_verse_order_entered_warning,  \
            'The no-verse-order message should be shown.'

    def test_bug_1404967(self):
        """
        Test for CCLI label showing correct text
        """
        # GIVEN; Mocked methods
        form = self.form
        # THEN: CCLI label should be CCLI song label
        self.assertNotEquals(form.ccli_label.text(), UiStrings().CCLINumberLabel,
                             'CCLI label should not be "{}"'.format(UiStrings().CCLINumberLabel))
        self.assertEquals(form.ccli_label.text(), UiStrings().CCLISongNumberLabel,
                          'CCLI label text should be "{}"'.format(UiStrings().CCLISongNumberLabel))

    def test_verse_order_lowercase(self):
        """
        Test that entering a verse order in lowercase automatically converts to uppercase
        """
        # GIVEN; Mocked methods
        form = self.form

        # WHEN: We enter a verse order in lowercase
        form.verse_order_edit.setText('v1 v2 c1 v3 c1 v4 c1')
        # Need to manually trigger this method as it is only triggered by manual input
        form.on_verse_order_text_changed(form.verse_order_edit.text())

        # THEN: The verse order should be converted to uppercase
        self.assertEqual(form.verse_order_edit.text(), 'V1 V2 C1 V3 C1 V4 C1')
Exemple #14
0
class SongMediaItem(MediaManagerItem):
    """
    This is the custom media manager item for Songs.
    """
    songs_go_live = QtCore.pyqtSignal(list)
    songs_add_to_service = QtCore.pyqtSignal(list)
    log.info('Song Media Item loaded')

    def __init__(self, parent, plugin):
        self.icon_path = 'songs/song'
        super(SongMediaItem, self).__init__(parent, plugin)

    def setup_item(self):
        """
        Do some additional setup.
        """
        self.songs_go_live.connect(self.go_live_remote)
        self.songs_add_to_service.connect(self.add_to_service_remote)
        self.single_service_item = False
        # Holds information about whether the edit is remotely triggered and which Song is required.
        self.remote_song = -1
        self.edit_item = None
        self.quick_preview_allowed = True
        self.has_search = True

    def _update_background_audio(self, song, item):
        song.media_files = []
        for i, bga in enumerate(item.background_audio):
            dest_file = os.path.join(
                AppLocation.get_section_data_path(self.plugin.name), 'audio', str(song.id), os.path.split(bga)[1])
            check_directory_exists(os.path.split(dest_file)[0])
            shutil.copyfile(os.path.join(AppLocation.get_section_data_path('servicemanager'), bga), dest_file)
            song.media_files.append(MediaFile.populate(weight=i, file_name=dest_file))
        self.plugin.manager.save_object(song, True)

    def add_end_header_bar(self):
        self.toolbar.addSeparator()
        # Song Maintenance Button
        self.maintenance_action = self.toolbar.add_toolbar_action('maintenance_action',
                                                                  icon=':/songs/song_maintenance.png',
                                                                  triggers=self.on_song_maintenance_click)
        self.add_search_to_toolbar()
        # Signals and slots
        Registry().register_function('songs_load_list', self.on_song_list_load)
        Registry().register_function('songs_preview', self.on_preview_click)
        self.search_text_edit.cleared.connect(self.on_clear_text_button_click)
        self.search_text_edit.searchTypeChanged.connect(self.on_search_text_button_clicked)

    def add_custom_context_actions(self):
        create_widget_action(self.list_view, separator=True)
        create_widget_action(
            self.list_view, text=translate('OpenLP.MediaManagerItem', '&Clone'), icon=':/general/general_clone.png',
            triggers=self.on_clone_click)

    def on_focus(self):
        self.search_text_edit.setFocus()
        self.search_text_edit.selectAll()

    def config_update(self):
        """
        Is triggered when the songs config is updated
        """
        log.debug('config_updated')
        self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
        self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
        self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
        self.display_songbook = Settings().value(self.settings_section + '/display songbook')
        self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol')

    def retranslateUi(self):
        self.search_text_label.setText('{text}:'.format(text=UiStrings().Search))
        self.search_text_button.setText(UiStrings().Search)
        self.maintenance_action.setText(SongStrings.SongMaintenance)
        self.maintenance_action.setToolTip(translate('SongsPlugin.MediaItem',
                                                     'Maintain the lists of authors, topics and books.'))

    def initialise(self):
        """
        Initialise variables when they cannot be initialised in the constructor.
        """
        self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
        self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
        self.open_lyrics = OpenLyrics(self.plugin.manager)
        self.search_text_edit.set_search_types([
            (SongSearch.Entire, ':/songs/song_search_all.png',
                translate('SongsPlugin.MediaItem', 'Entire Song'),
                translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
            (SongSearch.Titles, ':/songs/song_search_title.png',
                translate('SongsPlugin.MediaItem', 'Titles'),
                translate('SongsPlugin.MediaItem', 'Search Titles...')),
            (SongSearch.Lyrics, ':/songs/song_search_lyrics.png',
                translate('SongsPlugin.MediaItem', 'Lyrics'),
                translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
            (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
                translate('SongsPlugin.MediaItem', 'Search Authors...')),
            (SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics,
                translate('SongsPlugin.MediaItem', 'Search Topics...')),
            (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
                translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
            (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes),
            (SongSearch.Copyright, ':/songs/song_search_copy.png',
                translate('SongsPlugin.MediaItem', 'Copyright'),
                translate('SongsPlugin.MediaItem', 'Search Copyright...')),
            (SongSearch.CCLInumber, ':/songs/song_search_ccli.png',
                translate('SongsPlugin.MediaItem', 'CCLI number'),
                translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
        ])
        self.search_text_edit.set_current_search_type(
            Settings().value('{section}/last search type'.format(section=self.settings_section)))
        self.config_update()

    def on_search_text_button_clicked(self):
        # Save the current search type to the configuration.
        Settings().setValue('{section}/last search type'.format(section=self.settings_section),
                            self.search_text_edit.current_search_type())
        # Reload the list considering the new search type.
        search_keywords = str(self.search_text_edit.displayText())
        search_type = self.search_text_edit.current_search_type()
        if search_type == SongSearch.Entire:
            log.debug('Entire Song Search')
            search_results = self.search_entire(search_keywords)
            self.display_results_song(search_results)
        elif search_type == SongSearch.Titles:
            log.debug('Titles Search')
            search_string = '%{text}%'.format(text=clean_string(search_keywords))
            search_results = self.plugin.manager.get_all_objects(Song, Song.search_title.like(search_string))
            self.display_results_song(search_results)
        elif search_type == SongSearch.Lyrics:
            log.debug('Lyrics Search')
            search_string = '%{text}%'.format(text=clean_string(search_keywords))
            search_results = self.plugin.manager.get_all_objects(Song, Song.search_lyrics.like(search_string))
            self.display_results_song(search_results)
        elif search_type == SongSearch.Authors:
            log.debug('Authors Search')
            search_string = '%{text}%'.format(text=search_keywords)
            search_results = self.plugin.manager.get_all_objects(
                Author, Author.display_name.like(search_string))
            self.display_results_author(search_results)
        elif search_type == SongSearch.Topics:
            log.debug('Topics Search')
            search_string = '%{text}%'.format(text=search_keywords)
            search_results = self.plugin.manager.get_all_objects(
                Topic, Topic.name.like(search_string))
            self.display_results_topic(search_results)
        elif search_type == SongSearch.Books:
            log.debug('Songbook Search')
            search_keywords = search_keywords.rpartition(' ')
            search_book = '{text}%'.format(text=search_keywords[0])
            search_entry = '{text}%'.format(text=search_keywords[2])
            search_results = (self.plugin.manager.session.query(SongBookEntry.entry, Book.name, Song.title, Song.id)
                              .join(Song)
                              .join(Book)
                              .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry),
                                      Song.temporary.is_(False)).all())
            self.display_results_book(search_results)
        elif search_type == SongSearch.Themes:
            log.debug('Theme Search')
            search_string = '%{text}%'.format(text=search_keywords)
            search_results = self.plugin.manager.get_all_objects(
                Song, Song.theme_name.like(search_string))
            self.display_results_themes(search_results)
        elif search_type == SongSearch.Copyright:
            log.debug('Copyright Search')
            search_string = '%{text}%'.format(text=search_keywords)
            search_results = self.plugin.manager.get_all_objects(
                Song, and_(Song.copyright.like(search_string), Song.copyright != ''))
            self.display_results_song(search_results)
        elif search_type == SongSearch.CCLInumber:
            log.debug('CCLI number Search')
            search_string = '%{text}%'.format(text=search_keywords)
            search_results = self.plugin.manager.get_all_objects(
                Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''))
            self.display_results_cclinumber(search_results)
        self.check_search_result()

    def search_entire(self, search_keywords):
        search_string = '%{text}%'.format(text=clean_string(search_keywords))
        return self.plugin.manager.get_all_objects(
            Song, or_(Song.search_title.like(search_string), Song.search_lyrics.like(search_string),
                      Song.comments.like(search_string)))

    def on_song_list_load(self):
        """
        Handle the exit from the edit dialog and trigger remote updates of songs
        """
        log.debug('on_song_list_load - start')
        # Called to redisplay the song list screen edit from a search or from the exit of the Song edit dialog. If
        # remote editing is active Trigger it and clean up so it will not update again. Push edits to the service
        # manager to update items
        if self.edit_item and self.update_service_on_edit and not self.remote_triggered:
            item = self.build_service_item(self.edit_item)
            self.service_manager.replace_service_item(item)
        self.on_search_text_button_clicked()
        log.debug('on_song_list_load - finished')

    def display_results_song(self, search_results):
        """
        Display the song search results in the media manager list

        :param search_results: A list of db Song objects
        :return: None
        """
        def get_song_key(song):
            """Get the key to sort by"""
            return song.sort_key

        log.debug('display results Song')
        self.save_auto_select_id()
        self.list_view.clear()
        search_results.sort(key=get_song_key)
        for song in search_results:
            # Do not display temporary songs
            if song.temporary:
                continue
            author_list = [author.display_name for author in song.authors]
            text = create_separated_list(author_list) if author_list else song.title
            song_detail = '{title} ({author})'.format(title=song.title, author=text)
            song_name = QtWidgets.QListWidgetItem(song_detail)
            song_name.setData(QtCore.Qt.UserRole, song.id)
            self.list_view.addItem(song_name)
            # Auto-select the item if name has been set
            if song.id == self.auto_select_id:
                self.list_view.setCurrentItem(song_name)
        self.auto_select_id = -1

    def display_results_author(self, search_results):
        """
        Display the song search results in the media manager list, grouped by author

        :param search_results: A list of db Author objects
        :return: None
        """
        def get_author_key(author):
            """Get the key to sort by"""
            return get_natural_key(author.display_name)

        def get_song_key(song):
            """Get the key to sort by"""
            return song.sort_key

        log.debug('display results Author')
        self.list_view.clear()
        search_results.sort(key=get_author_key)
        for author in search_results:
            author.songs.sort(key=get_song_key)
            for song in author.songs:
                # Do not display temporary songs
                if song.temporary:
                    continue
                song_detail = '{author} ({title})'.format(author=author.display_name, title=song.title)
                song_name = QtWidgets.QListWidgetItem(song_detail)
                song_name.setData(QtCore.Qt.UserRole, song.id)
                self.list_view.addItem(song_name)

    def display_results_book(self, search_results):
        """
        Display the song search results in the media manager list, grouped by book and entry

        :param search_results: A tuple containing (songbook entry, book name, song title, song id)
        :return: None
        """
        def get_songbook_key(result):
            """Get the key to sort by"""
            return (get_natural_key(result[1]), get_natural_key(result[0]), get_natural_key(result[2]))

        log.debug('display results Book')
        self.list_view.clear()
        search_results.sort(key=get_songbook_key)
        for result in search_results:
            song_detail = '{result1} #{result0}: {result2}'.format(result1=result[1], result0=result[0],
                                                                   result2=result[2])
            song_name = QtWidgets.QListWidgetItem(song_detail)
            song_name.setData(QtCore.Qt.UserRole, result[3])
            self.list_view.addItem(song_name)

    def display_results_topic(self, search_results):
        """
        Display the song search results in the media manager list, grouped by topic

        :param search_results: A list of db Topic objects
        :return: None
        """
        def get_topic_key(topic):
            """Get the key to sort by"""
            return get_natural_key(topic.name)

        def get_song_key(song):
            """Get the key to sort by"""
            return song.sort_key

        log.debug('display results Topic')
        self.list_view.clear()
        search_results.sort(key=get_topic_key)
        for topic in search_results:
            topic.songs.sort(key=get_song_key)
            for song in topic.songs:
                # Do not display temporary songs
                if song.temporary:
                    continue
                song_detail = '{topic} ({title})'.format(topic=topic.name, title=song.title)
                song_name = QtWidgets.QListWidgetItem(song_detail)
                song_name.setData(QtCore.Qt.UserRole, song.id)
                self.list_view.addItem(song_name)

    def display_results_themes(self, search_results):
        """
        Display the song search results in the media manager list, sorted by theme

        :param search_results: A list of db Song objects
        :return: None
        """
        def get_theme_key(song):
            """Get the key to sort by"""
            return (get_natural_key(song.theme_name), song.sort_key)

        log.debug('display results Themes')
        self.list_view.clear()
        search_results.sort(key=get_theme_key)
        for song in search_results:
            # Do not display temporary songs
            if song.temporary:
                continue
            song_detail = '{theme} ({song})'.format(theme=song.theme_name, song=song.title)
            song_name = QtWidgets.QListWidgetItem(song_detail)
            song_name.setData(QtCore.Qt.UserRole, song.id)
            self.list_view.addItem(song_name)

    def display_results_cclinumber(self, search_results):
        """
        Display the song search results in the media manager list, sorted by CCLI number

        :param search_results: A list of db Song objects
        :return: None
        """
        def get_cclinumber_key(song):
            """Get the key to sort by"""
            return (get_natural_key(song.ccli_number), song.sort_key)

        log.debug('display results CCLI number')
        self.list_view.clear()
        search_results.sort(key=get_cclinumber_key)
        for song in search_results:
            # Do not display temporary songs
            if song.temporary:
                continue
            song_detail = '{ccli} ({song})'.format(ccli=song.ccli_number, song=song.title)
            song_name = QtWidgets.QListWidgetItem(song_detail)
            song_name.setData(QtCore.Qt.UserRole, song.id)
            self.list_view.addItem(song_name)

    def on_clear_text_button_click(self):
        """
        Clear the search text.
        """
        self.search_text_edit.clear()
        self.on_search_text_button_clicked()

    def on_search_text_edit_changed(self, text):
        """
        If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start
        till 7 characters have been entered.
        """
        if self.is_search_as_you_type_enabled:
            search_length = 1
            if self.search_text_edit.current_search_type() == SongSearch.Entire:
                search_length = 4
            elif self.search_text_edit.current_search_type() == SongSearch.Lyrics:
                search_length = 3
            if len(text) > search_length:
                self.on_search_text_button_clicked()
            elif not text:
                self.on_clear_text_button_click()

    def on_import_click(self):
        if not hasattr(self, 'import_wizard'):
            self.import_wizard = SongImportForm(self, self.plugin)
        self.import_wizard.exec()
        # Run song load as list may have been cancelled but some songs loaded
        Registry().execute('songs_load_list')

    def on_export_click(self):
        if not hasattr(self, 'export_wizard'):
            self.export_wizard = SongExportForm(self, self.plugin)
        self.export_wizard.exec()

    def on_new_click(self):
        log.debug('on_new_click')
        self.edit_song_form.new_song()
        self.edit_song_form.exec()
        self.on_clear_text_button_click()
        self.on_selection_change()
        self.auto_select_id = -1

    def on_song_maintenance_click(self):
        self.song_maintenance_form.exec()

    def on_remote_edit(self, song_id, preview=False):
        """
        Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator
        to say which type of display is required.
        """
        log.debug('on_remote_edit for song {song}'.format(song=song_id))
        song_id = int(song_id)
        valid = self.plugin.manager.get_object(Song, song_id)
        if valid:
            self.edit_song_form.load_song(song_id, preview)
            if self.edit_song_form.exec() == QtWidgets.QDialog.Accepted:
                self.auto_select_id = -1
                self.on_song_list_load()
                self.remote_song = song_id
                self.remote_triggered = True
                item = self.build_service_item(remote=True)
                self.remote_song = -1
                self.remote_triggered = None
                if item:
                    if preview:
                        # A song can only be edited if it comes from plugin, so we set it again for the new item.
                        item.from_plugin = True
                    return item
        return None

    def on_edit_click(self):
        """
        Edit a song
        """
        log.debug('on_edit_click')
        if check_item_selected(self.list_view, UiStrings().SelectEdit):
            self.edit_item = self.list_view.currentItem()
            item_id = self.edit_item.data(QtCore.Qt.UserRole)
            self.edit_song_form.load_song(item_id, False)
            self.edit_song_form.exec()
            self.auto_select_id = -1
            self.on_song_list_load()
        self.edit_item = None

    def on_delete_click(self):
        """
        Remove a song from the list and database
        """
        if check_item_selected(self.list_view, UiStrings().SelectDelete):
            items = self.list_view.selectedIndexes()
            if QtWidgets.QMessageBox.question(
                    self, UiStrings().ConfirmDelete,
                    translate('SongsPlugin.MediaItem',
                              'Are you sure you want to delete the "{items:d}" '
                              'selected song(s)?').format(items=len(items)),
                    QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
                    QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.No:
                return
            self.application.set_busy_cursor()
            self.main_window.display_progress_bar(len(items))
            for item in items:
                item_id = item.data(QtCore.Qt.UserRole)
                delete_song(item_id, self.plugin)
                self.main_window.increment_progress_bar()
            self.main_window.finished_progress_bar()
            self.application.set_normal_cursor()
            self.on_search_text_button_clicked()

    def on_clone_click(self):
        """
        Clone a Song
        """
        log.debug('on_clone_click')
        if check_item_selected(self.list_view, UiStrings().SelectEdit):
            self.edit_item = self.list_view.currentItem()
            item_id = self.edit_item.data(QtCore.Qt.UserRole)
            old_song = self.plugin.manager.get_object(Song, item_id)
            song_xml = self.open_lyrics.song_to_xml(old_song)
            new_song = self.open_lyrics.xml_to_song(song_xml)
            new_song.title = '{title} <{text}>'.format(title=new_song.title,
                                                       text=translate('SongsPlugin.MediaItem',
                                                                      'copy', 'For song cloning'))
            # Copy audio files from the old to the new song
            if len(old_song.media_files) > 0:
                save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(new_song.id))
                check_directory_exists(save_path)
                for media_file in old_song.media_files:
                    new_media_file_name = os.path.join(save_path, os.path.basename(media_file.file_name))
                    shutil.copyfile(media_file.file_name, new_media_file_name)
                    new_media_file = MediaFile()
                    new_media_file.file_name = new_media_file_name
                    new_media_file.type = media_file.type
                    new_media_file.weight = media_file.weight
                    new_song.media_files.append(new_media_file)
            self.plugin.manager.save_object(new_song)
        self.on_song_list_load()

    def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
                            context=ServiceItemContext.Service):
        """
        Generate the slide data. Needs to be implemented by the plugin.

        :param service_item: The service item to be built on
        :param item: The Song item to be used
        :param xml_version: The xml version (not used)
        :param remote: Triggered from remote
        :param context: Why is it being generated
        """
        log.debug('generate_slide_data: {service}, {item}, {remote}'.format(service=service_item, item=item,
                                                                            remote=self.remote_song))
        item_id = self._get_id_of_item_to_generate(item, self.remote_song)
        service_item.add_capability(ItemCapabilities.CanEdit)
        service_item.add_capability(ItemCapabilities.CanPreview)
        service_item.add_capability(ItemCapabilities.CanLoop)
        service_item.add_capability(ItemCapabilities.OnLoadUpdate)
        service_item.add_capability(ItemCapabilities.AddIfNewItem)
        service_item.add_capability(ItemCapabilities.CanSoftBreak)
        song = self.plugin.manager.get_object(Song, item_id)
        service_item.theme = song.theme_name
        service_item.edit_id = item_id
        verse_list = SongXML().get_verses(song.lyrics)
        # no verse list or only 1 space (in error)
        verse_tags_translated = False
        if VerseType.from_translated_string(str(verse_list[0][0]['type'])) is not None:
            verse_tags_translated = True
        if not song.verse_order.strip():
            for verse in verse_list:
                # We cannot use from_loose_input() here, because database is supposed to contain English lowercase
                # singlechar tags.
                verse_tag = verse[0]['type']
                verse_index = None
                if len(verse_tag) > 1:
                    verse_index = VerseType.from_translated_string(verse_tag)
                    if verse_index is None:
                        verse_index = VerseType.from_string(verse_tag, None)
                if verse_index is None:
                    verse_index = VerseType.from_tag(verse_tag)
                verse_tag = VerseType.translated_tags[verse_index].upper()
                verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
                service_item.add_from_text(str(verse[1]), verse_def)
        else:
            # Loop through the verse list and expand the song accordingly.
            for order in song.verse_order.lower().split():
                if not order:
                    break
                for verse in verse_list:
                    if verse[0]['type'][0].lower() == \
                            order[0] and (verse[0]['label'].lower() == order[1:] or not order[1:]):
                        if verse_tags_translated:
                            verse_index = VerseType.from_translated_tag(verse[0]['type'])
                        else:
                            verse_index = VerseType.from_tag(verse[0]['type'])
                        verse_tag = VerseType.translated_tags[verse_index]
                        verse_def = '{tag}{text}'.format(tag=verse_tag, text=verse[0]['label'])
                        service_item.add_from_text(verse[1], verse_def)
        service_item.title = song.title
        author_list = self.generate_footer(service_item, song)
        service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)}
        service_item.xml_version = self.open_lyrics.song_to_xml(song)
        # Add the audio file to the service item.
        if song.media_files:
            service_item.add_capability(ItemCapabilities.HasBackgroundAudio)
            service_item.background_audio = [m.file_name for m in song.media_files]
        return True

    def generate_footer(self, item, song):
        """
        Generates the song footer based on a song and adds details to a service item.

        :param item: The service item to be amended
        :param song: The song to be used to generate the footer
        :return: List of all authors (only required for initial song generation)
        """
        authors_words = []
        authors_music = []
        authors_words_music = []
        authors_translation = []
        authors_none = []
        for author_song in song.authors_songs:
            if author_song.author_type == AuthorType.Words:
                authors_words.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.Music:
                authors_music.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.WordsAndMusic:
                authors_words_music.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.Translation:
                authors_translation.append(author_song.author.display_name)
            else:
                authors_none.append(author_song.author.display_name)
        authors_all = authors_words_music + authors_words + authors_music + authors_translation + authors_none
        item.audit = [
            song.title, authors_all, song.copyright, str(song.ccli_number)
        ]
        item.raw_footer = []
        item.raw_footer.append(song.title)
        if authors_none:
            item.raw_footer.append("{text}: {authors}".format(text=translate('OpenLP.Ui', 'Written by'),
                                                              authors=create_separated_list(authors_none)))
        if authors_words_music:
            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.WordsAndMusic],
                                                              authors=create_separated_list(authors_words_music)))
        if authors_words:
            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Words],
                                                              authors=create_separated_list(authors_words)))
        if authors_music:
            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Music],
                                                              authors=create_separated_list(authors_music)))
        if authors_translation:
            item.raw_footer.append("{text}: {authors}".format(text=AuthorType.Types[AuthorType.Translation],
                                                              authors=create_separated_list(authors_translation)))
        if song.copyright:
            if self.display_copyright_symbol:
                item.raw_footer.append("{symbol} {song}".format(symbol=SongStrings.CopyrightSymbol,
                                                                song=song.copyright))
            else:
                item.raw_footer.append(song.copyright)
        if self.display_songbook and song.songbook_entries:
            songbooks = [str(songbook_entry) for songbook_entry in song.songbook_entries]
            item.raw_footer.append(", ".join(songbooks))
        if Settings().value('core/ccli number'):
            item.raw_footer.append(translate('SongsPlugin.MediaItem',
                                             'CCLI License: ') + Settings().value('core/ccli number'))
        return authors_all

    def service_load(self, item):
        """
        Triggered by a song being loaded by the service manager.
        """
        log.debug('service_load')
        if self.plugin.status != PluginStatus.Active or not item.data_string:
            return
        search_results = self.plugin.manager.get_all_objects(
            Song, Song.search_title == item.data_string['title'], Song.search_title.asc())
        edit_id = 0
        add_song = True
        if search_results:
            for song in search_results:
                if self._authors_match(song, item.data_string['authors']):
                    add_song = False
                    edit_id = song.id
                    break
                # If there's any backing tracks, copy them over.
                if item.background_audio:
                    self._update_background_audio(song, item)
        if add_song and self.add_song_from_service:
            song = self.open_lyrics.xml_to_song(item.xml_version)
            # If there's any backing tracks, copy them over.
            if item.background_audio:
                self._update_background_audio(song, item)
            edit_id = song.id
            self.on_search_text_button_clicked()
        elif add_song and not self.add_song_from_service:
            # Make sure we temporary import formatting tags.
            song = self.open_lyrics.xml_to_song(item.xml_version, True)
            # If there's any backing tracks, copy them over.
            if item.background_audio:
                self._update_background_audio(song, item)
            edit_id = song.id
        # Update service with correct song id and return it to caller.
        item.edit_id = edit_id
        self.generate_footer(item, song)
        return item

    def _authors_match(self, song, authors):
        """
        Checks whether authors from a song in the database match the authors of the song to be imported.

        :param song: A list of authors from the song in the database
        :param authors: A string with authors from the song to be imported
        :return: True when Authors do match, else False.
        """
        author_list = authors.split(', ')
        for author in song.authors:
            if author.display_name in author_list:
                author_list.remove(author.display_name)
            else:
                return False
        # List must be empty at the end
        return not author_list

    def search(self, string, show_error):
        """
        Search for some songs
        :param string: The string to show
        :param show_error: Is this an error?
        """
        search_results = self.search_entire(string)
        return [[song.id, song.title, song.alternate_title] for song in search_results]
Exemple #15
0
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the EditSongForm class
    """
    def setUp(self):
        """
        Create the UI
        """
        Registry.create()
        self.setup_application()
        self.main_window = QtGui.QMainWindow()
        Registry().register('main_window', self.main_window)
        Registry().register('theme_manager', MagicMock())
        self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        del self.form
        del self.main_window

    def ui_defaults_test(self):
        """
        Test that the EditSongForm defaults are correct
        """
        self.assertFalse(self.form.verse_edit_button.isEnabled(),
                         'The verse edit button should not be enabled')
        self.assertFalse(self.form.verse_delete_button.isEnabled(),
                         'The verse delete button should not be enabled')
        self.assertFalse(self.form.author_remove_button.isEnabled(),
                         'The author remove button should not be enabled')
        self.assertFalse(self.form.topic_remove_button.isEnabled(),
                         'The topic remove button should not be enabled')

    def is_verse_edit_form_executed_test(self):
        pass

    def verse_order_no_warning_test(self):
        """
        Test if the verse order warning is not shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1 V2'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(
            side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(
            return_value=given_verse_order.split())

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: No text should be shown.
        assert self.form.warning_label.text(
        ) == '', 'There should be no warning.'

    def verse_order_incomplete_warning_test(self):
        """
        Test if the verse-order-incomple warning is shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data.return_value = 'V1'
        second_verse = MagicMock()
        second_verse.data.return_value = 'V2'
        self.form.verse_list_widget.item = MagicMock(
            side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(
            return_value=[given_verse_order])

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The verse-order-incomplete text should be shown.
        assert self.form.warning_label.text() == self.form.not_all_verses_used_warning, \
            'The verse-order-incomplete warning should be shown.'

    def bug_1170435_test(self):
        """
        Regression test for bug 1170435 (test if "no verse order" message is shown)
        """
        # GIVEN: Mocked methods.
        given_verse_order = ''
        self.form.verse_list_widget.rowCount = MagicMock(return_value=1)
        # Mock out the verse. (We want a verse type to be returned).
        mocked_verse = MagicMock()
        mocked_verse.data.return_value = 'V1'
        self.form.verse_list_widget.item = MagicMock(return_value=mocked_verse)
        self.form._extract_verse_order = MagicMock(return_value=[])
        self.form.verse_order_edit.text = MagicMock(
            return_value=given_verse_order)
        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The no-verse-order message should be shown.
        assert self.form.warning_label.text() == self.form.no_verse_order_entered_warning,  \
            'The no-verse-order message should be shown.'

    def bug_1404967_test(self):
        """
        Test for CCLI label showing correct text
        """
        # GIVEN; Mocked methods
        form = self.form
        # THEN: CCLI label should be CCLI song label
        self.assertNotEquals(
            form.ccli_label.text(),
            UiStrings().CCLINumberLabel,
            'CCLI label should not be "{}"'.format(
                UiStrings().CCLINumberLabel))
        self.assertEquals(
            form.ccli_label.text(),
            UiStrings().CCLISongNumberLabel,
            'CCLI label text should be "{}"'.format(
                UiStrings().CCLISongNumberLabel))
Exemple #16
0
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the functions in the :mod:`lib` module.
    """
    def setUp(self):
        """
        Set up the components need for all tests.
        """
        Registry.create()
        Registry().register('service_list', MagicMock())
        Registry().register('main_window', MagicMock())
        with patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__', return_value=None):
            self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
        self.setup_application()
        self.build_settings()
        QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.destroy_settings()

    def test_validate_matching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")

    def test_validate_nonmatching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{br}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        self.assertTrue(valid, "The tags list should be valid")

    @patch('openlp.plugins.songs.forms.editsongform.set_case_insensitive_completer')
    def test_load_objects(self, mocked_set_case_insensitive_completer):
        """
        Test the _load_objects() method
        """
        # GIVEN: A song edit form and some mocked stuff
        mocked_class = MagicMock()
        mocked_class.name = 'Author'
        mocked_combo = MagicMock()
        mocked_combo.count.return_value = 0
        mocked_cache = MagicMock()
        mocked_object = MagicMock()
        mocked_object.name = 'Charles'
        mocked_object.id = 1
        mocked_manager = MagicMock()
        mocked_manager.get_all_objects.return_value = [mocked_object]
        self.edit_song_form.manager = mocked_manager

        # WHEN: _load_objects() is called
        self.edit_song_form._load_objects(mocked_class, mocked_combo, mocked_cache)

        # THEN: All the correct methods should have been called
        self.edit_song_form.manager.get_all_objects.assert_called_once_with(mocked_class)
        mocked_combo.clear.assert_called_once_with()
        mocked_combo.count.assert_called_once_with()
        mocked_combo.addItem.assert_called_once_with('Charles')
        mocked_cache.append.assert_called_once_with('Charles')
        mocked_combo.setItemData.assert_called_once_with(0, 1)
        mocked_set_case_insensitive_completer.assert_called_once_with(mocked_cache, mocked_combo)
        mocked_combo.setCurrentIndex.assert_called_once_with(-1)
        mocked_combo.setCurrentText.assert_called_once_with('')
class TestEditSongForm(TestCase):
    """
    Test the EditSongForm class
    """

    def setUp(self):
        """
        Create the UI
        """
        Registry.create()
        self.app = QtGui.QApplication([])
        self.main_window = QtGui.QMainWindow()
        Registry().register('main_window', self.main_window)
        Registry().register('theme_manager', MagicMock())
        self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        del self.form
        del self.main_window
        del self.app

    def ui_defaults_test(self):
        """
        Test that the EditSongForm defaults are correct
        """
        self.assertFalse(self.form.verse_edit_button.isEnabled(), 'The verse edit button should not be enabled')
        self.assertFalse(self.form.verse_delete_button.isEnabled(), 'The verse delete button should not be enabled')
        self.assertFalse(self.form.author_remove_button.isEnabled(), 'The author remove button should not be enabled')
        self.assertFalse(self.form.topic_remove_button.isEnabled(), 'The topic remove button should not be enabled')

    def is_verse_edit_form_executed_test(self):
        pass

    def verse_order_no_warning_test(self):
        """
        Test if the verse order warning is not shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1 V2'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data = MagicMock(return_value='V1')
        second_verse = MagicMock()
        second_verse.data = MagicMock(return_value= 'V2')
        self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(return_value=given_verse_order.split())

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: No text should be shown.
        assert self.form.warning_label.text() == '', 'There should be no warning.'

    def verse_order_incomplete_warning_test(self):
        """
        Test if the verse-order-incomple warning is shown
        """
        # GIVEN: Mocked methods.
        given_verse_order = 'V1'
        self.form.verse_list_widget.rowCount = MagicMock(return_value=2)
        # Mock out the verse.
        first_verse = MagicMock()
        first_verse.data = MagicMock(return_value='V1')
        second_verse = MagicMock()
        second_verse.data = MagicMock(return_value= 'V2')
        self.form.verse_list_widget.item = MagicMock(side_effect=[first_verse, second_verse])
        self.form._extract_verse_order = MagicMock(return_value=[given_verse_order])

        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The verse-order-incomplete text should be shown.
        assert self.form.warning_label.text() == self.form.not_all_verses_used_warning, \
            'The verse-order-incomplete warning should be shown.'

    def bug_1170435_test(self):
        """
        Regression test for bug 1170435 (test if "no verse order" message is shown)
        """
        # GIVEN: Mocked methods.
        given_verse_order = ''
        self.form.verse_list_widget.rowCount = MagicMock(return_value=1)
        # Mock out the verse. (We want a verse type to be returned).
        mocked_verse = MagicMock()
        mocked_verse.data = MagicMock(return_value='V1')
        self.form.verse_list_widget.item = MagicMock(return_value=mocked_verse)
        self.form._extract_verse_order = MagicMock(return_value=[])
        self.form.verse_order_edit.text = MagicMock(return_value=given_verse_order)
        # WHEN: Call the method.
        self.form.on_verse_order_text_changed(given_verse_order)

        # THEN: The no-verse-order message should be shown.
        assert self.form.warning_label.text() == self.form.no_verse_order_entered_warning,  \
            'The no-verse-order message should be shown.'
Exemple #18
0
class SongMediaItem(MediaManagerItem):
    """
    This is the custom media manager item for Songs.
    """
    log.info('Song Media Item loaded')

    def __init__(self, parent, plugin):
        self.icon_path = 'songs/song'
        super(SongMediaItem, self).__init__(parent, plugin)

    def setup_item(self):
        """
        Do some additional setup.
        """
        self.single_service_item = False
        # Holds information about whether the edit is remotely triggered and which Song is required.
        self.remote_song = -1
        self.edit_item = None
        self.quick_preview_allowed = True
        self.has_search = True

    def _update_background_audio(self, song, item):
        song.media_files = []
        for i, bga in enumerate(item.background_audio):
            dest_file = os.path.join(
                AppLocation.get_section_data_path(self.plugin.name), 'audio', str(song.id), os.path.split(bga)[1])
            check_directory_exists(os.path.split(dest_file)[0])
            shutil.copyfile(os.path.join(AppLocation.get_section_data_path('servicemanager'), bga), dest_file)
            song.media_files.append(MediaFile.populate(weight=i, file_name=dest_file))
        self.plugin.manager.save_object(song, True)

    def add_end_header_bar(self):
        self.toolbar.addSeparator()
        # Song Maintenance Button
        self.maintenance_action = self.toolbar.add_toolbar_action('maintenance_action',
                                                                  icon=':/songs/song_maintenance.png',
                                                                  triggers=self.on_song_maintenance_click)
        self.add_search_to_toolbar()
        # Signals and slots
        Registry().register_function('songs_load_list', self.on_song_list_load)
        Registry().register_function('songs_preview', self.on_preview_click)
        QtCore.QObject.connect(self.search_text_edit, QtCore.SIGNAL('cleared()'), self.on_clear_text_button_click)
        QtCore.QObject.connect(
            self.search_text_edit, QtCore.SIGNAL('searchTypeChanged(int)'), self.on_search_text_button_clicked)

    def add_custom_context_actions(self):
        create_widget_action(self.list_view, separator=True)
        create_widget_action(
            self.list_view, text=translate('OpenLP.MediaManagerItem', '&Clone'), icon=':/general/general_clone.png',
            triggers=self.on_clone_click)

    def on_focus(self):
        self.search_text_edit.setFocus()

    def config_update(self):
        """
        Is triggered when the songs config is updated
        """
        log.debug('config_updated')
        self.search_as_you_type = Settings().value(self.settings_section + '/search as type')
        self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
        self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
        self.display_songbook = Settings().value(self.settings_section + '/display songbook')
        self.display_copyright_symbol = Settings().value(self.settings_section + '/display copyright symbol')

    def retranslateUi(self):
        self.search_text_label.setText('%s:' % UiStrings().Search)
        self.search_text_button.setText(UiStrings().Search)
        self.maintenance_action.setText(SongStrings.SongMaintenance)
        self.maintenance_action.setToolTip(translate('SongsPlugin.MediaItem',
                                                     'Maintain the lists of authors, topics and books.'))

    def initialise(self):
        """
        Initialise variables when they cannot be initialised in the constructor.
        """
        self.song_maintenance_form = SongMaintenanceForm(self.plugin.manager, self)
        self.edit_song_form = EditSongForm(self, self.main_window, self.plugin.manager)
        self.open_lyrics = OpenLyrics(self.plugin.manager)
        self.search_text_edit.set_search_types([
            (SongSearch.Entire, ':/songs/song_search_all.png',
                translate('SongsPlugin.MediaItem', 'Entire Song'),
                translate('SongsPlugin.MediaItem', 'Search Entire Song...')),
            (SongSearch.Titles, ':/songs/song_search_title.png',
                translate('SongsPlugin.MediaItem', 'Titles'),
                translate('SongsPlugin.MediaItem', 'Search Titles...')),
            (SongSearch.Lyrics, ':/songs/song_search_lyrics.png',
                translate('SongsPlugin.MediaItem', 'Lyrics'),
                translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
            (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
                translate('SongsPlugin.MediaItem', 'Search Authors...')),
            (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
                translate('SongsPlugin.MediaItem', 'Search Song Books...')),
            (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes)
        ])
        self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
        self.config_update()

    def on_search_text_button_clicked(self):
        # Save the current search type to the configuration.
        Settings().setValue('%s/last search type' % self.settings_section, self.search_text_edit.current_search_type())
        # Reload the list considering the new search type.
        search_keywords = str(self.search_text_edit.displayText())
        search_type = self.search_text_edit.current_search_type()
        if search_type == SongSearch.Entire:
            log.debug('Entire Song Search')
            search_results = self.search_entire(search_keywords)
            self.display_results_song(search_results)
        elif search_type == SongSearch.Titles:
            log.debug('Titles Search')
            search_string = '%' + clean_string(search_keywords) + '%'
            search_results = self.plugin.manager.get_all_objects(Song, Song.search_title.like(search_string))
            self.display_results_song(search_results)
        elif search_type == SongSearch.Lyrics:
            log.debug('Lyrics Search')
            search_string = '%' + clean_string(search_keywords) + '%'
            search_results = self.plugin.manager.get_all_objects(Song, Song.search_lyrics.like(search_string))
            self.display_results_song(search_results)
        elif search_type == SongSearch.Authors:
            log.debug('Authors Search')
            search_string = '%' + search_keywords + '%'
            search_results = self.plugin.manager.get_all_objects(
                Author, Author.display_name.like(search_string), Author.display_name.asc())
            self.display_results_author(search_results)
        elif search_type == SongSearch.Books:
            log.debug('Books Search')
            search_string = '%' + search_keywords + '%'
            search_results = self.plugin.manager.get_all_objects(Book, Book.name.like(search_string), Book.name.asc())
            song_number = False
            if not search_results:
                search_keywords = search_keywords.rpartition(' ')
                search_string = '%' + search_keywords[0] + '%'
                search_results = self.plugin.manager.get_all_objects(Book,
                                                                     Book.name.like(search_string), Book.name.asc())
                song_number = re.sub(r'[^0-9]', '', search_keywords[2])
            self.display_results_book(search_results, song_number)
        elif search_type == SongSearch.Themes:
            log.debug('Theme Search')
            search_string = '%' + search_keywords + '%'
            search_results = self.plugin.manager.get_all_objects(Song, Song.theme_name.like(search_string))
            self.display_results_song(search_results)
        self.check_search_result()

    def search_entire(self, search_keywords):
        search_string = '%' + clean_string(search_keywords) + '%'
        return self.plugin.manager.get_all_objects(
            Song, or_(Song.search_title.like(search_string), Song.search_lyrics.like(search_string),
                      Song.comments.like(search_string)))

    def on_song_list_load(self):
        """
        Handle the exit from the edit dialog and trigger remote updates of songs
        """
        log.debug('on_song_list_load - start')
        # Called to redisplay the song list screen edit from a search or from the exit of the Song edit dialog. If
        # remote editing is active Trigger it and clean up so it will not update again. Push edits to the service
        # manager to update items
        if self.edit_item and self.update_service_on_edit and not self.remote_triggered:
            item = self.build_service_item(self.edit_item)
            self.service_manager.replace_service_item(item)
        self.on_search_text_button_clicked()
        log.debug('on_song_list_load - finished')

    def display_results_song(self, search_results):
        log.debug('display results Song')
        self.save_auto_select_id()
        self.list_view.clear()
        search_results.sort(key=lambda song: song.sort_key)
        for song in search_results:
            # Do not display temporary songs
            if song.temporary:
                continue
            author_list = [author.display_name for author in song.authors]
            song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title
            song_name = QtGui.QListWidgetItem(song_detail)
            song_name.setData(QtCore.Qt.UserRole, song.id)
            self.list_view.addItem(song_name)
            # Auto-select the item if name has been set
            if song.id == self.auto_select_id:
                self.list_view.setCurrentItem(song_name)
        self.auto_select_id = -1

    def display_results_author(self, search_results):
        log.debug('display results Author')
        self.list_view.clear()
        for author in search_results:
            for song in author.songs:
                # Do not display temporary songs
                if song.temporary:
                    continue
                song_detail = '%s (%s)' % (author.display_name, song.title)
                song_name = QtGui.QListWidgetItem(song_detail)
                song_name.setData(QtCore.Qt.UserRole, song.id)
                self.list_view.addItem(song_name)

    def display_results_book(self, search_results, song_number=False):
        log.debug('display results Book')
        self.list_view.clear()
        for book in search_results:
            songs = sorted(book.songs, key=lambda song: int(re.match(r'[0-9]+', '0' + song.song_number).group()))
            for song in songs:
                # Do not display temporary songs
                if song.temporary:
                    continue
                if song_number and song_number not in song.song_number:
                    continue
                song_detail = '%s - %s (%s)' % (book.name, song.song_number, song.title)
                song_name = QtGui.QListWidgetItem(song_detail)
                song_name.setData(QtCore.Qt.UserRole, song.id)
                self.list_view.addItem(song_name)

    def on_clear_text_button_click(self):
        """
        Clear the search text.
        """
        self.search_text_edit.clear()
        self.on_search_text_button_clicked()

    def on_search_text_edit_changed(self, text):
        """
        If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start
        till 7 characters have been entered.
        """
        if self.search_as_you_type:
            search_length = 1
            if self.search_text_edit.current_search_type() == SongSearch.Entire:
                search_length = 4
            elif self.search_text_edit.current_search_type() == SongSearch.Lyrics:
                search_length = 3
            if len(text) > search_length:
                self.on_search_text_button_clicked()
            elif not text:
                self.on_clear_text_button_click()

    def on_import_click(self):
        if not hasattr(self, 'import_wizard'):
            self.import_wizard = SongImportForm(self, self.plugin)
        self.import_wizard.exec_()
        # Run song load as list may have been cancelled but some songs loaded
        Registry().execute('songs_load_list')

    def on_export_click(self):
        if not hasattr(self, 'export_wizard'):
            self.export_wizard = SongExportForm(self, self.plugin)
        self.export_wizard.exec_()

    def on_new_click(self):
        log.debug('on_new_click')
        self.edit_song_form.new_song()
        self.edit_song_form.exec_()
        self.on_clear_text_button_click()
        self.on_selection_change()
        self.auto_select_id = -1

    def on_song_maintenance_click(self):
        self.song_maintenance_form.exec_()

    def on_remote_edit(self, song_id, preview=False):
        """
        Called by ServiceManager or SlideController by event passing the Song Id in the payload along with an indicator
        to say which type of display is required.
        """
        log.debug('on_remote_edit for song %s' % song_id)
        song_id = int(song_id)
        valid = self.plugin.manager.get_object(Song, song_id)
        if valid:
            self.edit_song_form.load_song(song_id, preview)
            if self.edit_song_form.exec_() == QtGui.QDialog.Accepted:
                self.auto_select_id = -1
                self.on_song_list_load()
                self.remote_song = song_id
                self.remote_triggered = True
                item = self.build_service_item(remote=True)
                self.remote_song = -1
                self.remote_triggered = None
                if item:
                    if preview:
                        # A song can only be edited if it comes from plugin, so we set it again for the new item.
                        item.from_plugin = True
                    return item
        return None


    def on_remote_refresh(self, song_id):
        song_id = int(song_id)
        valid = self.plugin.manager.get_object(Song, song_id)
        if valid:
            self.edit_song_form.load_song(song_id, False)
            self.edit_song_form.save_song()
            self.auto_select_id = -1
            self.on_song_list_load()
            self.remote_song = song_id
            self.remote_triggered = True
            item = self.build_service_item(remote=True)
            self.remote_song = -1
            self.remote_triggered = None
            if item:
                return item
        return None


    def on_edit_click(self):
        """
        Edit a song
        """
        log.debug('on_edit_click')
        if check_item_selected(self.list_view, UiStrings().SelectEdit):
            self.edit_item = self.list_view.currentItem()
            item_id = self.edit_item.data(QtCore.Qt.UserRole)
            self.edit_song_form.load_song(item_id, False)
            self.edit_song_form.exec_()
            self.auto_select_id = -1
            self.on_song_list_load()
        self.edit_item = None

    def on_delete_click(self):
        """
        Remove a song from the list and database
        """
        if check_item_selected(self.list_view, UiStrings().SelectDelete):
            items = self.list_view.selectedIndexes()
            if QtGui.QMessageBox.question(
                    self, UiStrings().ConfirmDelete,
                    translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '',
                              QtCore.QCoreApplication.CodecForTr, len(items)),
                    QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No),
                    QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No:
                return
            self.application.set_busy_cursor()
            self.main_window.display_progress_bar(len(items))
            for item in items:
                item_id = item.data(QtCore.Qt.UserRole)
                delete_song(item_id, self.plugin)
                self.main_window.increment_progress_bar()
            self.main_window.finished_progress_bar()
            self.application.set_normal_cursor()
            self.on_search_text_button_clicked()

    def on_clone_click(self):
        """
        Clone a Song
        """
        log.debug('on_clone_click')
        if check_item_selected(self.list_view, UiStrings().SelectEdit):
            self.edit_item = self.list_view.currentItem()
            item_id = self.edit_item.data(QtCore.Qt.UserRole)
            old_song = self.plugin.manager.get_object(Song, item_id)
            song_xml = self.open_lyrics.song_to_xml(old_song)
            new_song = self.open_lyrics.xml_to_song(song_xml)
            new_song.title = '%s <%s>' % \
                             (new_song.title, translate('SongsPlugin.MediaItem', 'copy', 'For song cloning'))
            # Copy audio files from the old to the new song
            if len(old_song.media_files) > 0:
                save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(new_song.id))
                check_directory_exists(save_path)
                for media_file in old_song.media_files:
                    new_media_file_name = os.path.join(save_path, os.path.basename(media_file.file_name))
                    shutil.copyfile(media_file.file_name, new_media_file_name)
                    new_media_file = MediaFile()
                    new_media_file.file_name = new_media_file_name
                    new_media_file.type = media_file.type
                    new_media_file.weight = media_file.weight
                    new_song.media_files.append(new_media_file)
            self.plugin.manager.save_object(new_song)
        self.on_song_list_load()

    def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
                            context=ServiceItemContext.Service):
        """
        Generate the slide data. Needs to be implemented by the plugin.

        :param service_item: The service item to be built on
        :param item: The Song item to be used
        :param xml_version: The xml version (not used)
        :param remote: Triggered from remote
        :param context: Why is it being generated
        """
        log.debug('generate_slide_data: %s, %s, %s' % (service_item, item, self.remote_song))
        item_id = self._get_id_of_item_to_generate(item, self.remote_song)
        service_item.add_capability(ItemCapabilities.CanEdit)
        service_item.add_capability(ItemCapabilities.CanPreview)
        service_item.add_capability(ItemCapabilities.CanLoop)
        service_item.add_capability(ItemCapabilities.OnLoadUpdate)
        service_item.add_capability(ItemCapabilities.AddIfNewItem)
        service_item.add_capability(ItemCapabilities.CanSoftBreak)
        song = self.plugin.manager.get_object(Song, item_id)
        service_item.theme = song.theme_name
        service_item.edit_id = item_id
        verse_list = SongXML().get_verses(song.lyrics)
        # no verse list or only 1 space (in error)
        verse_tags_translated = False
        if VerseType.from_translated_string(str(verse_list[0][0]['type'])) is not None:
            verse_tags_translated = True
        if not song.verse_order.strip():
            for verse in verse_list:
                # We cannot use from_loose_input() here, because database is supposed to contain English lowercase
                # singlechar tags.
                verse_tag = verse[0]['type']
                verse_index = None
                if len(verse_tag) > 1:
                    verse_index = VerseType.from_translated_string(verse_tag)
                    if verse_index is None:
                        verse_index = VerseType.from_string(verse_tag, None)
                if verse_index is None:
                    verse_index = VerseType.from_tag(verse_tag)
                verse_tag = VerseType.translated_tags[verse_index].upper()
                verse_def = '%s%s' % (verse_tag, verse[0]['label'])
                service_item.add_from_text(str(verse[1]), verse_def)
        else:
            # Loop through the verse list and expand the song accordingly.
            for order in song.verse_order.lower().split():
                if not order:
                    break
                for verse in verse_list:
                    if verse[0]['type'][0].lower() == \
                            order[0] and (verse[0]['label'].lower() == order[1:] or not order[1:]):
                        if verse_tags_translated:
                            verse_index = VerseType.from_translated_tag(verse[0]['type'])
                        else:
                            verse_index = VerseType.from_tag(verse[0]['type'])
                        verse_tag = VerseType.translated_tags[verse_index]
                        verse_def = '%s%s' % (verse_tag, verse[0]['label'])
                        service_item.add_from_text(verse[1], verse_def)
        service_item.title = song.title
        author_list = self.generate_footer(service_item, song)
        service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)}
        service_item.set_extra_data_dict(self.open_lyrics.song_to_line_dict(song))
        service_item.xml_version = self.open_lyrics.song_to_xml(song)
        # Add the audio file to the service item.
        if song.media_files:
            service_item.add_capability(ItemCapabilities.HasBackgroundAudio)
            service_item.background_audio = [m.file_name for m in song.media_files]
        return True

    def generate_footer(self, item, song):
        """
        Generates the song footer based on a song and adds details to a service item.

        :param item: The service item to be amended
        :param song: The song to be used to generate the footer
        :return: List of all authors (only required for initial song generation)
        """
        authors_words = []
        authors_music = []
        authors_words_music = []
        authors_translation = []
        authors_none = []
        for author_song in song.authors_songs:
            if author_song.author_type == AuthorType.Words:
                authors_words.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.Music:
                authors_music.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.WordsAndMusic:
                authors_words_music.append(author_song.author.display_name)
            elif author_song.author_type == AuthorType.Translation:
                authors_translation.append(author_song.author.display_name)
            else:
                authors_none.append(author_song.author.display_name)
        authors_all = authors_words_music + authors_words + authors_music + authors_translation + authors_none
        item.audit = [
            song.title, authors_all, song.copyright, str(song.ccli_number)
        ]
        item.raw_footer = []
        item.raw_footer.append(song.title)
        if authors_none:
            item.raw_footer.append("%s: %s" % (translate('OpenLP.Ui', 'Written by'),
                                               create_separated_list(authors_none)))
        if authors_words_music:
            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.WordsAndMusic],
                                               create_separated_list(authors_words_music)))
        if authors_words:
            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Words],
                                               create_separated_list(authors_words)))
        if authors_music:
            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Music],
                                               create_separated_list(authors_music)))
        if authors_translation:
            item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Translation],
                                               create_separated_list(authors_translation)))
        if song.copyright:
            if self.display_copyright_symbol:
                item.raw_footer.append("%s %s" % (SongStrings.CopyrightSymbol, song.copyright))
            else:
                item.raw_footer.append(song.copyright)
        if self.display_songbook and song.book:
            item.raw_footer.append("%s #%s" % (song.book.name, song.song_number))
        if Settings().value('core/ccli number'):
            item.raw_footer.append(translate('SongsPlugin.MediaItem',
                                             'CCLI License: ') + Settings().value('core/ccli number'))
        return authors_all

    def service_load(self, item):
        """
        Triggered by a song being loaded by the service manager.
        """
        log.debug('service_load')
        if self.plugin.status != PluginStatus.Active or not item.data_string:
            return
        search_results = self.plugin.manager.get_all_objects(
            Song, Song.search_title == item.data_string['title'], Song.search_title.asc())
        edit_id = 0
        add_song = True
        if search_results:
            for song in search_results:
                if self._authors_match(song, item.data_string['authors']):
                    add_song = False
                    edit_id = song.id
                    break
                # If there's any backing tracks, copy them over.
                if item.background_audio:
                    self._update_background_audio(song, item)
        if add_song and self.add_song_from_service:
            song = self.open_lyrics.xml_to_song(item.xml_version)
            # If there's any backing tracks, copy them over.
            if item.background_audio:
                self._update_background_audio(song, item)
            edit_id = song.id
            self.on_search_text_button_clicked()
        elif add_song and not self.add_song_from_service:
            # Make sure we temporary import formatting tags.
            song = self.open_lyrics.xml_to_song(item.xml_version, True)
            # If there's any backing tracks, copy them over.
            if item.background_audio:
                self._update_background_audio(song, item)
            edit_id = song.id
        # Update service with correct song id and return it to caller.
        item.edit_id = edit_id
        self.generate_footer(item, song)
        return item

    def _authors_match(self, song, authors):
        """
        Checks whether authors from a song in the database match the authors of the song to be imported.

        :param song: A list of authors from the song in the database
        :param authors: A string with authors from the song to be imported
        :return: True when Authors do match, else False.
        """
        author_list = authors.split(', ')
        for author in song.authors:
            if author.display_name in author_list:
                author_list.remove(author.display_name)
            else:
                return False
        # List must be empty at the end
        return not author_list

    def search(self, string, show_error):
        """
        Search for some songs
        :param string: The string to show
        :param show_error: Is this an error?
        """
        search_results = self.search_entire(string)
        return [[song.id, song.title] for song in search_results]
Exemple #19
0
class TestEditSongForm(TestCase, TestMixin):
    """
    Test the functions in the :mod:`lib` module.
    """
    def setUp(self):
        """
        Set up the components need for all tests.
        """
        Registry.create()
        Registry().register('service_list', MagicMock())
        Registry().register('main_window', MagicMock())
        with patch(
                'openlp.plugins.songs.forms.editsongform.EditSongForm.__init__',
                return_value=None):
            self.edit_song_form = EditSongForm(None, MagicMock(), MagicMock())
        self.setup_application()
        self.build_settings()
        QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.destroy_settings()

    def test_validate_matching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        assert valid is True, "The tags list should be valid"

    def test_validate_nonmatching_tags(self):
        # Given a set of tags
        tags = ['{r}', '{/r}', '{bl}', '{/bl}', '{br}', '{su}', '{/su}']

        # WHEN we validate them
        valid = self.edit_song_form._validate_tags(tags)

        # THEN they should be valid
        assert valid is True, "The tags list should be valid"

    @patch(
        'openlp.plugins.songs.forms.editsongform.set_case_insensitive_completer'
    )
    def test_load_objects(self, mocked_set_case_insensitive_completer):
        """
        Test the _load_objects() method
        """
        # GIVEN: A song edit form and some mocked stuff
        mocked_class = MagicMock()
        mocked_class.name = 'Author'
        mocked_combo = MagicMock()
        mocked_combo.count.return_value = 0
        mocked_cache = MagicMock()
        mocked_object = MagicMock()
        mocked_object.name = 'Charles'
        mocked_object.id = 1
        mocked_manager = MagicMock()
        mocked_manager.get_all_objects.return_value = [mocked_object]
        self.edit_song_form.manager = mocked_manager

        # WHEN: _load_objects() is called
        self.edit_song_form._load_objects(mocked_class, mocked_combo,
                                          mocked_cache)

        # THEN: All the correct methods should have been called
        self.edit_song_form.manager.get_all_objects.assert_called_once_with(
            mocked_class)
        mocked_combo.clear.assert_called_once_with()
        mocked_combo.count.assert_called_once_with()
        mocked_combo.addItem.assert_called_once_with('Charles')
        mocked_cache.append.assert_called_once_with('Charles')
        mocked_combo.setItemData.assert_called_once_with(0, 1)
        mocked_set_case_insensitive_completer.assert_called_once_with(
            mocked_cache, mocked_combo)
        mocked_combo.setCurrentIndex.assert_called_once_with(-1)
        mocked_combo.setCurrentText.assert_called_once_with('')