← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/cleanups02182 into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/cleanups02182 into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)
Related bugs:
  Bug #1016078 in OpenLP: "Can't clear preview pane"
  https://bugs.launchpad.net/openlp/+bug/1016078
  Bug #1449477 in OpenLP: "lyric title via api contoller text"
  https://bugs.launchpad.net/openlp/+bug/1449477
  Bug #1512689 in OpenLP: "automatically created slides"
  https://bugs.launchpad.net/openlp/+bug/1512689
  Bug #1544260 in OpenLP: "Moving Song in Service Manager, Hover Text says "Service Manager""
  https://bugs.launchpad.net/openlp/+bug/1544260
  Bug #1651823 in OpenLP: "Make it possible to clone a Custom Slide"
  https://bugs.launchpad.net/openlp/+bug/1651823

For more details, see:
https://code.launchpad.net/~trb143/openlp/cleanups02182/+merge/341268

Add some small items from wish lists
Various cleanups.

--------------------------------------------------------------------------------
lp:~trb143/openlp/cleanups02182 (revision 2832)
https://ci.openlp.io/job/Branch-01-Pull/2469/                          [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2370/                  [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/157/                   [FAILURE]
https://ci.openlp.io/job/Branch-03a-Build-Source/79/                   [SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/73/                    [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Analysis/1541/                [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1354/                [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/287/                 [RUNNING]

-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/cleanups02182 into lp:openlp.
=== modified file 'openlp/core/api/endpoint/controller.py'
--- openlp/core/api/endpoint/controller.py	2017-12-29 09:15:48 +0000
+++ openlp/core/api/endpoint/controller.py	2018-03-09 21:16:28 +0000
@@ -91,6 +91,7 @@
                 item['text'] = str(frame['title'])
                 item['html'] = str(frame['title'])
             item['selected'] = (live_controller.selected_row == index)
+            item['title'] = current_item.title
             data.append(item)
     json_data = {'results': {'slides': data}}
     if current_item:
@@ -117,12 +118,11 @@
     return {'results': {'success': True}}
 
 
-@controller_endpoint.route('{action:next|previous}')
+@controller_endpoint.route('{controller}/{action:next|previous}')
 @requires_auth
 def controller_direction(request, controller, action):
     """
     Handles requests for setting service items in the slide controller
-11
     :param request: The http request object.
     :param controller: the controller slides forward or backward.
     :param action: the controller slides forward or backward.
@@ -137,7 +137,7 @@
 def controller_direction_api(request, controller, action):
     """
     Handles requests for setting service items in the slide controller
-11
+
     :param request: The http request object.
     :param controller: the controller slides forward or backward.
     :param action: the controller slides forward or backward.

=== modified file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py	2018-02-11 21:52:04 +0000
+++ openlp/core/common/__init__.py	2018-03-09 21:16:28 +0000
@@ -62,7 +62,7 @@
     """
     # Get the local IPv4 active address(es) that are NOT localhost (lo or '127.0.0.1')
     log.debug('Getting local IPv4 interface(es) information')
-    MY_IP4 = {}
+    my_ip4 = {}
     for iface in QNetworkInterface.allInterfaces():
         if not iface.isValid() or not (iface.flags() & (QNetworkInterface.IsUp | QNetworkInterface.IsRunning)):
             continue
@@ -70,25 +70,25 @@
             ip = address.ip()
             # NOTE: Next line will skip if interface is localhost - keep for now until we decide about it later
             # if (ip.protocol() == QAbstractSocket.IPv4Protocol) and (ip != QHostAddress.LocalHost):
-            if (ip.protocol() == QAbstractSocket.IPv4Protocol):
-                MY_IP4[iface.name()] = {'ip': ip.toString(),
+            if ip.protocol() == QAbstractSocket.IPv4Protocol:
+                my_ip4[iface.name()] = {'ip': ip.toString(),
                                         'broadcast': address.broadcast().toString(),
                                         'netmask': address.netmask().toString(),
                                         'prefix': address.prefixLength(),
                                         'localnet': QHostAddress(address.netmask().toIPv4Address() &
-                                                                ip.toIPv4Address()).toString()
+                                                                 ip.toIPv4Address()).toString()
                                         }
                 log.debug('Adding {iface} to active list'.format(iface=iface.name()))
-    if len(MY_IP4) == 1:
-        if 'lo' in MY_IP4:
+    if len(my_ip4) == 1:
+        if 'lo' in my_ip4:
             # No active interfaces - so leave localhost in there
             log.warning('No active IPv4 interfaces found except localhost')
     else:
         # Since we have a valid IP4 interface, remove localhost
         log.debug('Found at least one IPv4 interface, removing localhost')
-        MY_IP4.pop('lo')
+        my_ip4.pop('lo')
 
-    return MY_IP4
+    return my_ip4
 
 
 def trace_error_handler(logger):

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2018-01-21 07:40:26 +0000
+++ openlp/core/lib/serviceitem.py	2018-03-09 21:16:28 +0000
@@ -121,6 +121,9 @@
 
     ``HasThumbnails``
             The item has related thumbnails available
+
+    ``HasMetaData``
+            The item has Meta Data about item
     """
     CanPreview = 1
     CanEdit = 2
@@ -143,6 +146,7 @@
     HasDisplayTitle = 19
     HasNotes = 20
     HasThumbnails = 21
+    HasMetaData = 22
 
 
 class ServiceItem(RegistryProperties):
@@ -200,6 +204,7 @@
         self.will_auto_start = False
         self.has_original_files = True
         self._new_item()
+        self.metadata = []
 
     def _new_item(self):
         """
@@ -375,7 +380,8 @@
             'background_audio': self.background_audio,
             'theme_overwritten': self.theme_overwritten,
             'will_auto_start': self.will_auto_start,
-            'processor': self.processor
+            'processor': self.processor,
+            'metadata': self.metadata
         }
         service_data = []
         if self.service_item_type == ServiceItemType.Text:
@@ -426,6 +432,7 @@
         self.will_auto_start = header.get('will_auto_start', False)
         self.processor = header.get('processor', None)
         self.has_original_files = True
+        self.metadata = header.get('item_meta_data', [])
         if 'background_audio' in header:
             self.background_audio = []
             for file_path in header['background_audio']:

=== modified file 'openlp/core/ui/firsttimewizard.py'
--- openlp/core/ui/firsttimewizard.py	2018-01-13 23:24:26 +0000
+++ openlp/core/ui/firsttimewizard.py	2018-03-09 21:16:28 +0000
@@ -127,9 +127,6 @@
         self.media_check_box.setChecked(True)
         self.media_check_box.setObjectName('media_check_box')
         self.plugin_layout.addWidget(self.media_check_box)
-        self.remote_check_box = QtWidgets.QCheckBox(self.plugin_page)
-        self.remote_check_box.setObjectName('remote_check_box')
-        self.plugin_layout.addWidget(self.remote_check_box)
         self.song_usage_check_box = QtWidgets.QCheckBox(self.plugin_page)
         self.song_usage_check_box.setChecked(True)
         self.song_usage_check_box.setObjectName('song_usage_check_box')
@@ -138,13 +135,6 @@
         self.alert_check_box.setChecked(True)
         self.alert_check_box.setObjectName('alert_check_box')
         self.plugin_layout.addWidget(self.alert_check_box)
-        self.projectors_check_box = QtWidgets.QCheckBox(self.plugin_page)
-        # If visibility setting for projector panel is True, check the box.
-        if Settings().value('projector/show after wizard'):
-            self.projectors_check_box.setChecked(True)
-        self.projectors_check_box.setObjectName('projectors_check_box')
-        self.projectors_check_box.clicked.connect(self.on_projectors_check_box_clicked)
-        self.plugin_layout.addWidget(self.projectors_check_box)
         first_time_wizard.setPage(FirstTimePage.Plugins, self.plugin_page)
         # The song samples page
         self.songs_page = QtWidgets.QWizardPage()
@@ -256,13 +246,9 @@
         self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
                                                       'Presentations – Show .ppt, .odp and .pdf files'))
         self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media – Playback of Audio and Video files'))
-        self.remote_check_box.setText(str(UiStrings().WebDownloadText))
         self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
         self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
                                                'Alerts – Display informative messages while showing other slides'))
-        self.projectors_check_box.setText(translate('OpenLP.FirstTimeWizard',
-                                                    'Projector Controller – Control PJLink compatible projects on your'
-                                                    ' network from OpenLP'))
         self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection'))
         self.no_internet_page.setSubTitle(
             translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.'))

=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py	2017-12-29 09:15:48 +0000
+++ openlp/core/ui/media/mediacontroller.py	2018-03-09 21:16:28 +0000
@@ -179,7 +179,6 @@
         """
         Check to see if we have any media Player's available.
         """
-        log.debug('_check_available_media_players')
         controller_dir = os.path.join('core', 'ui', 'media')
         # Find all files that do not begin with '.' (lp:#1738047) and end with player.py
         glob_pattern = os.path.join(controller_dir, '[!.]*player.py')

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2018-02-03 14:17:46 +0000
+++ openlp/core/ui/servicemanager.py	2018-03-09 21:16:28 +0000
@@ -751,7 +751,7 @@
 
     def context_menu(self, point):
         """
-        The Right click context menu from the Serviceitem list
+        The Right click context menu from the Service item list
 
         :param point: The location of the cursor.
         """
@@ -1136,7 +1136,6 @@
     def on_delete_from_service(self):
         """
         Remove the current ServiceItem from the list.
-        :param field:
         """
         item = self.find_service_item()[0]
         if item != -1:
@@ -1205,6 +1204,9 @@
                 tips.append('<strong>{text1}: </strong> {text2}'.format(text1=text1, text2=text2))
             if item['service_item'].is_capable(ItemCapabilities.HasVariableStartTime):
                 tips.append(item['service_item'].get_media_time())
+            if item['service_item'].is_capable(ItemCapabilities.HasMetaData):
+                for meta in item['service_item'].metadata:
+                    tips.append(meta)
             tree_widget_item.setToolTip(0, '<br>'.join(tips))
             tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
             tree_widget_item.setSelected(item['selected'])
@@ -1362,7 +1364,6 @@
     def make_preview(self):
         """
         Send the current item to the Preview slide controller
-        :param field:
         """
         self.application.set_busy_cursor()
         item, child = self.find_service_item()
@@ -1387,7 +1388,6 @@
     def on_double_click_live(self):
         """
         Send the current item to the Live slide controller but triggered by a tablewidget click event.
-        :param field:
         """
         self.list_double_clicked = True
         self.make_live()
@@ -1396,7 +1396,6 @@
         """
         If single click previewing is enabled, and triggered by a tablewidget click event,
         start a timeout to verify a double-click hasn't triggered.
-        :param field:
         """
         if Settings().value('advanced/single click service preview'):
             if not self.list_double_clicked:
@@ -1407,7 +1406,6 @@
     def on_single_click_preview_timeout(self):
         """
         If a single click ok, but double click not triggered, send the current item to the Preview slide controller.
-        :param field:
         """
         if self.list_double_clicked:
             # If a double click has registered, clear it.
@@ -1447,7 +1445,6 @@
     def remote_edit(self):
         """
         Triggers a remote edit to a plugin to allow item to be edited.
-        :param field:
         """
         item = self.find_service_item()[0]
         if self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEdit):
@@ -1459,8 +1456,6 @@
     def on_service_item_rename(self):
         """
         Opens a dialog to rename the service item.
-
-        :param field: Not used, but PyQt needs this.
         """
         item = self.find_service_item()[0]
         if not self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEditTitle):
@@ -1477,7 +1472,6 @@
     def create_custom(self):
         """
         Saves the current text item as a custom slide
-        :param field:
         """
         item = self.find_service_item()[0]
         Registry().execute('custom_create_from_service', self.service_items[item]['service_item'])
@@ -1597,8 +1591,6 @@
     def on_theme_change_action(self):
         """
         Handles theme change events
-
-        :param field:
         """
         theme = self.sender().objectName()
         # No object name means that the "Default" theme is supposed to be used.

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2018-02-03 11:32:49 +0000
+++ openlp/core/ui/slidecontroller.py	2018-03-09 21:16:28 +0000
@@ -318,6 +318,10 @@
                                             tooltip=translate('OpenLP.SlideController',
                                                               'Edit and reload song preview.'),
                                             triggers=self.on_edit_song)
+            self.toolbar.add_toolbar_action('clear', icon=':/general/general_delete.png',
+                                            tooltip=translate('OpenLP.SlideController',
+                                                              'Clear'),
+                                            triggers=self.on_clear)
         self.controller_layout.addWidget(self.toolbar)
         # Build the Media Toolbar
         self.media_controller.register_controller(self)
@@ -356,7 +360,7 @@
             self.audio_time_label.setObjectName('audio_time_label')
             self.toolbar.add_toolbar_widget(self.audio_time_label)
             self.toolbar.set_widget_visible(AUDIO_LIST, False)
-            self.toolbar.set_widget_visible(['song_menu'], False)
+            self.toolbar.set_widget_visible('song_menu', False)
         # Screen preview area
         self.preview_frame = QtWidgets.QFrame(self.splitter)
         self.preview_frame.setGeometry(QtCore.QRect(0, 0, 300, 300 * self.ratio))
@@ -427,7 +431,8 @@
             self.__add_actions_to_widget(self.controller)
         else:
             self.preview_widget.doubleClicked.connect(self.on_preview_double_click)
-            self.toolbar.set_widget_visible(['editSong'], False)
+            self.toolbar.set_widget_visible('editSong', False)
+            self.toolbar.set_widget_visible('clear', False)
             self.controller.addActions([self.next_item, self.previous_item])
         Registry().register_function('slidecontroller_{text}_stop_loop'.format(text=self.type_prefix),
                                      self.on_stop_loop)
@@ -726,7 +731,7 @@
         self.mediabar.hide()
         self.song_menu.hide()
         self.toolbar.set_widget_visible(LOOP_LIST, False)
-        self.toolbar.set_widget_visible(['song_menu'], False)
+        self.toolbar.set_widget_visible('song_menu', False)
         # Reset the button
         self.play_slides_once.setChecked(False)
         self.play_slides_once.setIcon(build_icon(':/media/media_time.png'))
@@ -737,7 +742,7 @@
         if item.is_text():
             if (Settings().value(self.main_window.songs_settings_section + '/display songbar') and
                     not self.song_menu.menu().isEmpty()):
-                self.toolbar.set_widget_visible(['song_menu'], True)
+                self.toolbar.set_widget_visible('song_menu', True)
         if item.is_capable(ItemCapabilities.CanLoop) and len(item.get_frames()) > 1:
             self.toolbar.set_widget_visible(LOOP_LIST)
         if item.is_media():
@@ -762,9 +767,10 @@
         # See bug #791050
         self.toolbar.hide()
         self.mediabar.hide()
-        self.toolbar.set_widget_visible(['editSong'], False)
+        self.toolbar.set_widget_visible('editSong', False)
+        self.toolbar.set_widget_visible('clear', True)
         if item.is_capable(ItemCapabilities.CanEdit) and item.from_plugin:
-            self.toolbar.set_widget_visible(['editSong'])
+            self.toolbar.set_widget_visible('editSong')
         elif item.is_media():
             self.mediabar.show()
         self.previous_item.setVisible(not item.is_media())
@@ -1381,6 +1387,14 @@
         if new_item:
             self.add_service_item(new_item)
 
+    def on_clear(self):
+        """
+        Clear the preview bar.
+        """
+        self.preview_widget.clear_list()
+        self.toolbar.set_widget_visible("editSong", False)
+        self.toolbar.set_widget_visible("clear", False)
+
     def on_preview_add_to_service(self):
         """
         From the preview display request the Item to be added to service

=== modified file 'openlp/core/widgets/toolbar.py'
--- openlp/core/widgets/toolbar.py	2017-12-29 09:15:48 +0000
+++ openlp/core/widgets/toolbar.py	2018-03-09 21:16:28 +0000
@@ -67,14 +67,20 @@
         """
         Set the visibility for a widget or a list of widgets.
 
-        :param widgets: A list of string with widget object names.
+        :param widgets: A list of strings or individual string with widget object names.
         :param visible: The new state as bool.
         """
-        for handle in widgets:
-            if handle in self.actions:
-                self.actions[handle].setVisible(visible)
+        if isinstance(widgets, list):
+            for handle in widgets:
+                if handle in self.actions:
+                    self.actions[handle].setVisible(visible)
+                else:
+                    log.warning('No handle "%s" in actions list.', str(handle))
+        else:
+            if widgets in self.actions:
+                self.actions[widgets].setVisible(visible)
             else:
-                log.warning('No handle "%s" in actions list.', str(handle))
+                log.warning('No handle "%s" in actions list.', str(widgets))
 
     def set_widget_enabled(self, widgets, enabled=True):
         """

=== modified file 'openlp/core/widgets/views.py'
--- openlp/core/widgets/views.py	2017-12-29 09:15:48 +0000
+++ openlp/core/widgets/views.py	2018-03-09 21:16:28 +0000
@@ -146,6 +146,14 @@
         self.screen_ratio = screen_ratio
         self.__recalculate_layout()
 
+    def clear_list(self):
+        """
+        Clear the preview list
+        :return:
+        """
+        self.setRowCount(0)
+        self.clear()
+
     def replace_service_item(self, service_item, width, slide_number):
         """
         Replace the current preview items with the ones in service_item and display the given slide
@@ -156,8 +164,7 @@
         """
         self.service_item = service_item
         self.setRowCount(0)
-        self.clear()
-        self.setColumnWidth(0, width)
+        self.clear_list()
         row = 0
         text = []
         for frame_number, frame in enumerate(self.service_item.get_frames()):

=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
--- openlp/plugins/custom/lib/mediaitem.py	2017-12-29 09:15:48 +0000
+++ openlp/plugins/custom/lib/mediaitem.py	2018-03-09 21:16:28 +0000
@@ -30,6 +30,7 @@
 from openlp.core.common.settings import Settings
 from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext, PluginStatus, \
     check_item_selected
+from openlp.core.lib.ui import create_widget_action
 from openlp.plugins.custom.forms.editcustomform import EditCustomForm
 from openlp.plugins.custom.lib import CustomXMLParser, CustomXMLBuilder
 from openlp.plugins.custom.lib.db import CustomSlide
@@ -84,6 +85,12 @@
         Registry().register_function('custom_preview', self.on_preview_click)
         Registry().register_function('custom_create_from_service', self.create_from_service_item)
 
+    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 config_update(self):
         """
         Config has been updated so reload values
@@ -243,6 +250,23 @@
             service_item.raw_footer.append('')
         return True
 
+    def on_clone_click(self):
+        """
+        Clone the selected Custom item
+        """
+        item = self.list_view.currentItem()
+        item_id = item.data(QtCore.Qt.UserRole)
+        old_custom_slide = self.plugin.db_manager.get_object(CustomSlide, item_id)
+        new_custom_slide = CustomSlide()
+        new_custom_slide.title = '{title} <{text}>'.format(title=old_custom_slide.title,
+                                                           text=translate('SongsPlugin.MediaItem',
+                                                                          'copy', 'For song cloning'))
+        new_custom_slide.text = old_custom_slide.text
+        new_custom_slide.credits = old_custom_slide.credits
+        new_custom_slide.theme_name = old_custom_slide.theme_name
+        self.plugin.db_manager.save_object(new_custom_slide)
+        self.on_search_text_button_clicked()
+
     def on_search_text_button_clicked(self):
         """
         Search the plugin database

=== modified file 'openlp/plugins/songs/lib/db.py'
--- openlp/plugins/songs/lib/db.py	2017-12-29 09:15:48 +0000
+++ openlp/plugins/songs/lib/db.py	2018-03-09 21:16:28 +0000
@@ -164,7 +164,7 @@
         """
         Add a Songbook Entry to the song if it not yet exists
 
-        :param songbook_name: Name of the Songbook.
+        :param songbook: Name of the Songbook.
         :param entry: Entry in the Songbook (usually a number)
         """
         for songbook_entry in self.songbook_entries:

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2018-01-19 21:31:36 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2018-03-09 21:16:28 +0000
@@ -572,10 +572,19 @@
         service_item.add_capability(ItemCapabilities.OnLoadUpdate)
         service_item.add_capability(ItemCapabilities.AddIfNewItem)
         service_item.add_capability(ItemCapabilities.CanSoftBreak)
+        service_item.add_capability(ItemCapabilities.HasMetaData)
         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)
+        if Settings().value('songs/add songbook slide') and song.songbook_entries:
+            first_slide = "\n"
+            for songbook_entry in song.songbook_entries:
+                first_slide = first_slide + "{book}/{num}/{pub}\n\n".format(book=songbook_entry.songbook.name,
+                                                                            num=songbook_entry.entry,
+                                                                            pub=songbook_entry.songbook.publisher)
+
+            service_item.add_from_text(first_slide, "O1")
         # 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:
@@ -622,6 +631,9 @@
         if song.media_files:
             service_item.add_capability(ItemCapabilities.HasBackgroundAudio)
             service_item.background_audio = [m.file_path for m in song.media_files]
+            item.metadata.append("<em>{label}:</em> {media}".
+                                 format(label=translate('SongsPlugin.MediaItem', 'Media'),
+                                        media=service_item.background_audio))
         return True
 
     def generate_footer(self, item, song):
@@ -685,6 +697,23 @@
         if Settings().value('core/ccli number'):
             item.raw_footer.append(translate('SongsPlugin.MediaItem',
                                              'CCLI License: ') + Settings().value('core/ccli number'))
+        item.metadata.append("<em>{label}:</em> {title}".format(label=translate('SongsPlugin.MediaItem', 'Title'),
+                                                                title=song.title))
+        if song.alternate_title:
+            item.metadata.append("<em>{label}:</em> {title}".
+                                 format(label=translate('SongsPlugin.MediaItem', 'Alt Title'),
+                                        title=song.alternate_title))
+        if song.songbook_entries:
+            for songbook_entry in song.songbook_entries:
+                item.metadata.append("<em>{label}:</em> {book}/{num}/{pub}".
+                                     format(label=translate('SongsPlugin.MediaItem', 'Songbook'),
+                                            book=songbook_entry.songbook.name,
+                                            num=songbook_entry.entry,
+                                            pub=songbook_entry.songbook.publisher))
+        if song.topics:
+            for topics in song.topics:
+                item.metadata.append("<em>{label}:</em> {topic}".
+                                     format(label=translate('SongsPlugin.MediaItem', 'Topic'), topic=topics.name))
         return authors_all
 
     def service_load(self, item):

=== modified file 'openlp/plugins/songs/lib/songstab.py'
--- openlp/plugins/songs/lib/songstab.py	2017-12-29 09:15:48 +0000
+++ openlp/plugins/songs/lib/songstab.py	2018-03-09 21:16:28 +0000
@@ -51,6 +51,9 @@
         self.add_from_service_check_box = QtWidgets.QCheckBox(self.mode_group_box)
         self.add_from_service_check_box.setObjectName('add_from_service_check_box')
         self.mode_layout.addWidget(self.add_from_service_check_box)
+        self.songbook_slide_check_box = QtWidgets.QCheckBox(self.mode_group_box)
+        self.songbook_slide_check_box.setObjectName('songbook_slide_check_box')
+        self.mode_layout.addWidget(self.songbook_slide_check_box)
         self.display_songbook_check_box = QtWidgets.QCheckBox(self.mode_group_box)
         self.display_songbook_check_box.setObjectName('songbook_check_box')
         self.mode_layout.addWidget(self.display_songbook_check_box)
@@ -95,6 +98,7 @@
         self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
         self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
         self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
+        self.songbook_slide_check_box.stateChanged.connect(self.on_songbook_slide_check_box_changed)
         self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
         self.display_written_by_check_box.stateChanged.connect(self.on_written_by_check_box_changed)
         self.display_copyright_check_box.stateChanged.connect(self.on_copyright_check_box_changed)
@@ -111,6 +115,8 @@
         self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
         self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab',
                                                           'Import missing songs from Service files'))
+        self.songbook_slide_check_box.setText(translate('SongsPlugin.SongsTab',
+                                                        'Add Songbooks as first side'))
         self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
         self.display_written_by_check_box.setText(translate(
             'SongsPlugin.SongsTab', 'Show "Written by:" in footer for unspecified authors'))
@@ -141,6 +147,9 @@
     def on_add_from_service_check_box_changed(self, check_state):
         self.update_load = (check_state == QtCore.Qt.Checked)
 
+    def on_songbook_slide_check_box_changed(self, check_state):
+        self.songbook_slide = (check_state == QtCore.Qt.Checked)
+
     def on_songbook_check_box_changed(self, check_state):
         self.display_songbook = (check_state == QtCore.Qt.Checked)
 
@@ -171,6 +180,7 @@
         self.tool_bar = settings.value('display songbar')
         self.update_edit = settings.value('update service on edit')
         self.update_load = settings.value('add song from service')
+        self.songbook_slide = settings.value('add songbook slide')
         self.display_songbook = settings.value('display songbook')
         self.display_written_by = settings.value('display written by')
         self.display_copyright_symbol = settings.value('display copyright symbol')
@@ -208,6 +218,7 @@
         settings.setValue('mainview chords', self.mainview_chords)
         settings.setValue('disable chords import', self.disable_chords_import)
         settings.setValue('chord notation', self.chord_notation)
+        settings.setValue('add songbook slide', self.songbook_slide)
         settings.endGroup()
         if self.tab_visited:
             self.settings_form.register_post_process('songs_config_updated')

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2017-12-29 09:15:48 +0000
+++ openlp/plugins/songs/songsplugin.py	2018-03-09 21:16:28 +0000
@@ -61,6 +61,7 @@
     'songs/last import type': SongFormat.OpenLyrics,
     'songs/update service on edit': False,
     'songs/add song from service': True,
+    'songs/add songbook slide': False,
     'songs/display songbar': True,
     'songs/display songbook': False,
     'songs/display written by': True,

=== modified file 'scripts/websocket_client.py'
--- scripts/websocket_client.py	2017-12-29 09:15:48 +0000
+++ scripts/websocket_client.py	2018-03-09 21:16:28 +0000
@@ -25,6 +25,7 @@
 import websockets
 import random
 
+
 async def tester():
     async with websockets.connect('ws://localhost:4317/poll') as websocket:
 

=== modified file 'tests/functional/openlp_core/api/endpoint/test_controller.py'
--- tests/functional/openlp_core/api/endpoint/test_controller.py	2018-02-03 14:41:18 +0000
+++ tests/functional/openlp_core/api/endpoint/test_controller.py	2018-03-09 21:16:28 +0000
@@ -20,10 +20,10 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 from unittest import TestCase
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock
 
 from openlp.core.common.registry import Registry
-from openlp.core.api.endpoint.controller import controller_text
+from openlp.core.api.endpoint.controller import controller_text, controller_direction
 
 
 class TestController(TestCase):
@@ -42,7 +42,7 @@
 
     def test_controller_text(self):
         """
-        Remote Deploy tests - test the dummy zip file is processed correctly
+        Remote API Tests : test the controller text method can be called
         """
         # GIVEN: A mocked service with a dummy service item
         self.mocked_live_controller.service_item = MagicMock()
@@ -52,3 +52,25 @@
         results = ret['results']
         assert isinstance(results['item'], MagicMock)
         assert len(results['slides']) == 0
+
+    def test_controller_direction_next(self):
+        """
+        Text the live next method is triggered
+        """
+        # GIVEN: A mocked service with a dummy service item
+        self.mocked_live_controller.service_item = MagicMock()
+        # WHEN: I trigger the method
+        controller_direction(None, "live", "next")
+        # THEN: The correct method is called
+        self.mocked_live_controller.slidecontroller_live_next.emit.assert_called_once_with()
+
+    def test_controller_direction_previous(self):
+        """
+        Text the live next method is triggered
+        """
+        # GIVEN: A mocked service with a dummy service item
+        self.mocked_live_controller.service_item = MagicMock()
+        # WHEN: I trigger the method
+        controller_direction(None, "live", "previous")
+        # THEN: The correct method is called
+        self.mocked_live_controller.slidecontroller_live_previous.emit.assert_called_once_with()

=== modified file 'tests/functional/openlp_plugins/songs/test_mediaitem.py'
--- tests/functional/openlp_plugins/songs/test_mediaitem.py	2017-12-29 09:15:48 +0000
+++ tests/functional/openlp_plugins/songs/test_mediaitem.py	2018-03-09 21:16:28 +0000
@@ -431,10 +431,12 @@
         # GIVEN: A Song and a Service Item
         song = Song()
         song.title = 'My Song'
+        song.alternate_title = ""
         song.copyright = 'My copyright'
         song.authors_songs = []
         song.songbook_entries = []
         song.ccli_number = ''
+        song.topics = None
         book1 = MagicMock()
         book1.name = "My songbook"
         book2 = MagicMock()


Follow ups