diff --git a/.bzrignore b/.bzrignore
index b91d4fc93..6377150e0 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -6,6 +6,8 @@
*.ropeproject
*.e4*
.eric4project
+.komodotools
+*.komodoproject
list
openlp.org 2.0.e4*
documentation/build/html
diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py
index 1014c994d..d67c05c42 100644
--- a/openlp/core/lib/db.py
+++ b/openlp/core/lib/db.py
@@ -194,6 +194,7 @@ class Manager(object):
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
except (SQLAlchemyError, DBAPIError):
log.exception('Error loading database: %s', self.db_url)
+ return
if db_ver > up_ver:
critical_error_message_box(
translate('OpenLP.Manager', 'Database Error'),
@@ -215,7 +216,7 @@ class Manager(object):
Save an object to the database
:param object_instance: The object to save
- :param commit: Commit the session with this object
+ :param commit: Commit the session with this object
"""
for try_count in range(3):
try:
diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py
index f1b875e6b..b9b5f5997 100644
--- a/openlp/core/ui/firsttimeform.py
+++ b/openlp/core/ui/firsttimeform.py
@@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
Run the wizard.
"""
- self.setDefaults()
+ self.set_defaults()
return QtGui.QWizard.exec_(self)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set up display at start of theme edit.
"""
diff --git a/openlp/core/ui/pluginform.py b/openlp/core/ui/pluginform.py
index 91b98b97a..78bdee4a5 100644
--- a/openlp/core/ui/pluginform.py
+++ b/openlp/core/ui/pluginform.py
@@ -30,7 +30,6 @@
The actual plugin view form
"""
import logging
-import os
from PyQt4 import QtGui
diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py
index d9b61e117..321071c49 100644
--- a/openlp/core/ui/themeform.py
+++ b/openlp/core/ui/themeform.py
@@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
self.footer_font_combo_box.activated.connect(self.update_theme)
self.footer_size_spin_box.valueChanged.connect(self.update_theme)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@@ -261,7 +261,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
log.debug('Editing theme %s' % self.theme.theme_name)
self.temp_background_filename = ''
self.update_theme_allowed = False
- self.setDefaults()
+ self.set_defaults()
self.update_theme_allowed = True
self.theme_name_label.setVisible(not edit)
self.theme_name_edit.setVisible(not edit)
diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py
index 05951d14a..5815457b5 100644
--- a/openlp/core/ui/wizard.py
+++ b/openlp/core/ui/wizard.py
@@ -197,7 +197,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
"""
Run the wizard.
"""
- self.setDefaults()
+ self.set_defaults()
return QtGui.QWizard.exec_(self)
def reject(self):
diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py
index ee5bee2d0..79b0bc699 100644
--- a/openlp/plugins/bibles/forms/bibleimportform.py
+++ b/openlp/plugins/bibles/forms/bibleimportform.py
@@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard):
self.license_details_page.registerField('license_copyright', self.copyright_edit)
self.license_details_page.registerField('license_permissions', self.permissions_edit)
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default values for the wizard pages.
"""
diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py
index d9936dfe6..09c0942b7 100644
--- a/openlp/plugins/bibles/forms/bibleupgradeform.py
+++ b/openlp/plugins/bibles/forms/bibleupgradeform.py
@@ -307,7 +307,7 @@ class BibleUpgradeForm(OpenLPWizard):
if self.currentPage() == self.progress_page:
return True
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default values for the wizard pages.
"""
diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py
index 5a10a14ae..4241b34dc 100644
--- a/openlp/plugins/remotes/lib/httprouter.py
+++ b/openlp/plugins/remotes/lib/httprouter.py
@@ -149,11 +149,11 @@ class HttpRouter(RegistryProperties):
"""
Initialise the router stack and any other variables.
"""
- authcode = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
+ auth_code = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
try:
- self.auth = base64.b64encode(authcode)
+ self.auth = base64.b64encode(auth_code)
except TypeError:
- self.auth = base64.b64encode(authcode.encode()).decode()
+ self.auth = base64.b64encode(auth_code.encode()).decode()
self.routes = [
('^/$', {'function': self.serve_file, 'secure': False}),
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
@@ -376,7 +376,6 @@ class HttpRouter(RegistryProperties):
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
Returns the extension and the content_type
"""
- content_type = 'text/plain'
ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type
@@ -439,7 +438,7 @@ class HttpRouter(RegistryProperties):
if plugin.status == PluginStatus.Active:
try:
text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
text = urllib.parse.unquote(text)
self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text])
@@ -453,6 +452,7 @@ class HttpRouter(RegistryProperties):
"""
Perform an action on the slide controller.
"""
+ log.debug("controller_text var = %s" % var)
current_item = self.live_controller.service_item
data = []
if current_item:
@@ -488,7 +488,7 @@ class HttpRouter(RegistryProperties):
if self.request_data:
try:
data = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
log.info(data)
# This slot expects an int within a list.
@@ -547,7 +547,7 @@ class HttpRouter(RegistryProperties):
"""
try:
text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
+ except KeyError:
return self.do_http_error()
text = urllib.parse.unquote(text)
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
@@ -563,12 +563,12 @@ class HttpRouter(RegistryProperties):
Go live on an item of type ``plugin``.
"""
try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ request_id = json.loads(self.request_data)['request']['id']
+ except KeyError:
return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
- plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [id, True])
+ plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [request_id, True])
return self.do_http_success()
def add_to_service(self, plugin_name):
@@ -576,11 +576,11 @@ class HttpRouter(RegistryProperties):
Add item of type ``plugin_name`` to the end of the service.
"""
try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
+ request_id = json.loads(self.request_data)['request']['id']
+ except KeyError:
return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item:
- item_id = plugin.media_item.create_item_from_id(id)
+ item_id = plugin.media_item.create_item_from_id(request_id)
plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
self.do_http_success()
diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py
index 22d0349f8..9a904090d 100644
--- a/openlp/plugins/remotes/lib/httpserver.py
+++ b/openlp/plugins/remotes/lib/httpserver.py
@@ -40,7 +40,7 @@ import time
from PyQt4 import QtCore
-from openlp.core.common import AppLocation, Settings
+from openlp.core.common import AppLocation, Settings, RegistryProperties
from openlp.plugins.remotes.lib import HttpRouter
@@ -94,13 +94,18 @@ class HttpThread(QtCore.QThread):
"""
self.http_server.start_server()
+ def stop(self):
+ log.debug("stop called")
+ self.http_server.stop = True
-class OpenLPServer():
+
+class OpenLPServer(RegistryProperties):
def __init__(self):
"""
Initialise the http server, and start the server of the correct type http / https
"""
- log.debug('Initialise httpserver')
+ super(OpenLPServer, self).__init__()
+ log.debug('Initialise OpenLP')
self.settings_section = 'remotes'
self.http_thread = HttpThread(self)
self.http_thread.start()
@@ -110,32 +115,49 @@ class OpenLPServer():
Start the correct server and save the handler
"""
address = Settings().value(self.settings_section + '/ip address')
- if Settings().value(self.settings_section + '/https enabled'):
+ self.address = address
+ self.is_secure = Settings().value(self.settings_section + '/https enabled')
+ self.needs_authentication = Settings().value(self.settings_section + '/authentication enabled')
+ if self.is_secure:
port = Settings().value(self.settings_section + '/https port')
- self.httpd = HTTPSServer((address, port), CustomHandler)
- log.debug('Started ssl httpd...')
+ self.port = port
+ self.start_server_instance(address, port, HTTPSServer)
else:
port = Settings().value(self.settings_section + '/port')
- loop = 1
- while loop < 3:
- try:
- self.httpd = ThreadingHTTPServer((address, port), CustomHandler)
- except OSError:
- loop += 1
- time.sleep(0.1)
- except:
- log.error('Failed to start server ')
- log.debug('Started non ssl httpd...')
+ self.port = port
+ self.start_server_instance(address, port, ThreadingHTTPServer)
if hasattr(self, 'httpd') and self.httpd:
self.httpd.serve_forever()
else:
log.debug('Failed to start server')
+ def start_server_instance(self, address, port, server_class):
+ """
+ Start the server
+
+ :param address: The server address
+ :param port: The run port
+ :param server_class: the class to start
+ """
+ loop = 1
+ while loop < 4:
+ try:
+ self.httpd = server_class((address, port), CustomHandler)
+ log.debug("Server started for class %s %s %d" % (server_class, address, port))
+ except OSError:
+ log.debug("failed to start http server thread state %d %s" %
+ (loop, self.http_thread.isRunning()))
+ loop += 1
+ time.sleep(0.1)
+ except:
+ log.error('Failed to start server ')
+
def stop_server(self):
"""
Stop the server
"""
- self.http_thread.exit(0)
+ if self.http_thread.isRunning():
+ self.http_thread.stop()
self.httpd = None
log.debug('Stopped the server.')
diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py
index d6b96cc1c..4db25cfc2 100644
--- a/openlp/plugins/remotes/lib/remotetab.py
+++ b/openlp/plugins/remotes/lib/remotetab.py
@@ -32,7 +32,7 @@ import os.path
from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.common import AppLocation, Settings, translate
-from openlp.core.lib import SettingsTab
+from openlp.core.lib import SettingsTab, build_icon
ZERO_URL = '0.0.0.0'
@@ -234,6 +234,7 @@ class RemoteTab(SettingsTab):
"""
Load the configuration and update the server configuration if necessary
"""
+ self.is_secure = Settings().value(self.settings_section + '/https enabled')
self.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + '/https port'))
self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
@@ -263,9 +264,7 @@ class RemoteTab(SettingsTab):
Settings().value(self.settings_section + '/port') != self.port_spin_box.value() or \
Settings().value(self.settings_section + '/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + '/https enabled') != \
- self.https_settings_group_box.isChecked() or \
- Settings().value(self.settings_section + '/authentication enabled') != \
- self.user_login_group_box.isChecked():
+ self.https_settings_group_box.isChecked():
self.settings_form.register_post_process('remotes_config_updated')
Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + '/https port', self.https_port_spin_box.value())
@@ -275,6 +274,7 @@ class RemoteTab(SettingsTab):
Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text())
+ self.generate_icon()
def on_twelve_hour_check_box_changed(self, check_state):
"""
@@ -290,3 +290,25 @@ class RemoteTab(SettingsTab):
Invert the HTTP group box based on Https group settings
"""
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
+
+ def generate_icon(self):
+ """
+ Generate icon for main window
+ """
+ self.remote_server_icon.hide()
+ icon = QtGui.QImage(':/remote/network_server.png')
+ icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ if self.is_secure:
+ overlay = QtGui.QImage(':/remote/network_ssl.png')
+ overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ painter = QtGui.QPainter(icon)
+ painter.drawImage(0, 0, overlay)
+ painter.end()
+ if Settings().value(self.settings_section + '/authentication enabled'):
+ overlay = QtGui.QImage(':/remote/network_auth.png')
+ overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
+ painter = QtGui.QPainter(icon)
+ painter.drawImage(20, 0, overlay)
+ painter.end()
+ self.remote_server_icon.setPixmap(QtGui.QPixmap.fromImage(icon))
+ self.remote_server_icon.show()
diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py
index d3dc6e58a..582192df4 100644
--- a/openlp/plugins/remotes/remoteplugin.py
+++ b/openlp/plugins/remotes/remoteplugin.py
@@ -28,7 +28,8 @@
###############################################################################
import logging
-import time
+
+from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
@@ -67,6 +68,21 @@ class RemotesPlugin(Plugin):
log.debug('initialise')
super(RemotesPlugin, self).initialise()
self.server = OpenLPServer()
+ if not hasattr(self, 'remote_server_icon'):
+ self.remote_server_icon = QtGui.QLabel(self.main_window.status_bar)
+ size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
+ size_policy.setHorizontalStretch(0)
+ size_policy.setVerticalStretch(0)
+ size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
+ self.remote_server_icon.setSizePolicy(size_policy)
+ self.remote_server_icon.setFrameShadow(QtGui.QFrame.Plain)
+ self.remote_server_icon.setLineWidth(1)
+ self.remote_server_icon.setScaledContents(True)
+ self.remote_server_icon.setFixedSize(20, 20)
+ self.remote_server_icon.setObjectName('remote_server_icon')
+ self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
+ self.settings_tab.remote_server_icon = self.remote_server_icon
+ self.settings_tab.generate_icon()
def finalise(self):
"""
@@ -104,9 +120,11 @@ class RemotesPlugin(Plugin):
def config_update(self):
"""
- Called when Config is changed to restart the server on new address or port
+ Called when Config is changed to requests a restart with the server on new address or port
"""
log.debug('remote config changed')
- self.finalise()
- time.sleep(0.5)
- self.initialise()
+ QtGui.QMessageBox.information(self.main_window,
+ translate('RemotePlugin', 'Server Config Change'),
+ translate('RemotePlugin', 'Server configuration changes will require a restart '
+ 'to take effect.'),
+ QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))
diff --git a/openlp/plugins/songs/forms/duplicatesongremovalform.py b/openlp/plugins/songs/forms/duplicatesongremovalform.py
index c99dee4a7..c411c8c1c 100644
--- a/openlp/plugins/songs/forms/duplicatesongremovalform.py
+++ b/openlp/plugins/songs/forms/duplicatesongremovalform.py
@@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True
self.plugin.media_item.on_search_text_button_clicked()
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default form values for the song import wizard.
"""
diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py
index f2ef5af06..d0fb51a2d 100644
--- a/openlp/plugins/songs/forms/editsongdialog.py
+++ b/openlp/plugins/songs/forms/editsongdialog.py
@@ -118,13 +118,18 @@ class Ui_EditSongDialog(object):
self.authors_group_box.setObjectName('authors_group_box')
self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box)
self.authors_layout.setObjectName('authors_layout')
- self.author_add_layout = QtGui.QHBoxLayout()
+ self.author_add_layout = QtGui.QVBoxLayout()
self.author_add_layout.setObjectName('author_add_layout')
+ self.author_type_layout = QtGui.QHBoxLayout()
+ self.author_type_layout.setObjectName('author_type_layout')
self.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box')
self.author_add_layout.addWidget(self.authors_combo_box)
+ self.author_types_combo_box = create_combo_box(self.authors_group_box, 'author_types_combo_box', editable=False)
+ self.author_type_layout.addWidget(self.author_types_combo_box)
self.author_add_button = QtGui.QPushButton(self.authors_group_box)
self.author_add_button.setObjectName('author_add_button')
- self.author_add_layout.addWidget(self.author_add_button)
+ self.author_type_layout.addWidget(self.author_add_button)
+ self.author_add_layout.addLayout(self.author_type_layout)
self.authors_layout.addLayout(self.author_add_layout)
self.authors_list_view = QtGui.QListWidget(self.authors_group_box)
self.authors_list_view.setAlternatingRowColors(True)
@@ -330,7 +335,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', 'Warning: You have not entered a verse order.')
-def create_combo_box(parent, name):
+def create_combo_box(parent, name, editable=True):
"""
Utility method to generate a standard combo box for this dialog.
@@ -340,7 +345,7 @@ def create_combo_box(parent, name):
combo_box = QtGui.QComboBox(parent)
combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
- combo_box.setEditable(True)
+ combo_box.setEditable(editable)
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name)
return combo_box
diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py
index 60c6eae78..1814655ea 100644
--- a/openlp/plugins/songs/forms/editsongform.py
+++ b/openlp/plugins/songs/forms/editsongform.py
@@ -42,7 +42,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStri
from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list
from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile
+from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorSong, AuthorType, Topic, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
@@ -122,12 +122,12 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
combo.setItemData(row, obj.id)
set_case_insensitive_completer(cache, combo)
- def _add_author_to_list(self, author):
+ def _add_author_to_list(self, author, author_type):
"""
Add an author to the author list.
"""
- author_item = QtGui.QListWidgetItem(str(author.display_name))
- author_item.setData(QtCore.Qt.UserRole, author.id)
+ author_item = QtGui.QListWidgetItem(author.get_display_name(author_type))
+ author_item.setData(QtCore.Qt.UserRole, (author.id, author_type))
self.authors_list_view.addItem(author_item)
def _extract_verse_order(self, verse_order):
@@ -217,8 +217,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
if self.authors_list_view.count() == 0:
self.song_tab_widget.setCurrentIndex(1)
self.authors_list_view.setFocus()
- critical_error_message_box(
- message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.'))
+ critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
+ 'You need to have an author for this song.'))
return False
if self.verse_order_edit.text():
result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount())
@@ -302,6 +302,15 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.authors.append(author.display_name)
set_case_insensitive_completer(self.authors, self.authors_combo_box)
+ # Types
+ self.author_types_combo_box.clear()
+ self.author_types_combo_box.addItem('')
+ # Don't iterate over the dictionary to give them this specific order
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Words], AuthorType.Words)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Music], AuthorType.Music)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.WordsAndMusic], AuthorType.WordsAndMusic)
+ self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Translation], AuthorType.Translation)
+
def load_topics(self):
"""
Load the topics into the combobox.
@@ -454,10 +463,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.tag_rows()
# clear the results
self.authors_list_view.clear()
- for author in self.song.authors:
- author_name = QtGui.QListWidgetItem(str(author.display_name))
- author_name.setData(QtCore.Qt.UserRole, author.id)
- self.authors_list_view.addItem(author_name)
+ for author_song in self.song.authors_songs:
+ self._add_author_to_list(author_song.author, author_song.author_type)
# clear the results
self.topics_list_view.clear()
for topic in self.song.topics:
@@ -496,6 +503,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
"""
item = int(self.authors_combo_box.currentIndex())
text = self.authors_combo_box.currentText().strip(' \r\n\t')
+ author_type = self.author_types_combo_box.itemData(self.author_types_combo_box.currentIndex())
# This if statement is for OS X, which doesn't seem to work well with
# the QCompleter auto-completion class. See bug #812628.
if text in self.authors:
@@ -513,7 +521,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
display_name=text)
self.manager.save_object(author)
- self._add_author_to_list(author)
+ self._add_author_to_list(author, author_type)
self.load_authors()
self.authors_combo_box.setCurrentIndex(0)
else:
@@ -521,11 +529,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0:
item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id)
- if self.authors_list_view.findItems(str(author.display_name), QtCore.Qt.MatchExactly):
+ if self.authors_list_view.findItems(author.get_display_name(author_type), QtCore.Qt.MatchExactly):
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else:
- self._add_author_to_list(author)
+ self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0)
else:
QtGui.QMessageBox.warning(
@@ -905,13 +913,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
else:
self.song.theme_name = None
self._process_lyrics()
- self.song.authors = []
+ self.song.authors_songs = []
for row in range(self.authors_list_view.count()):
item = self.authors_list_view.item(row)
- author_id = (item.data(QtCore.Qt.UserRole))
- author = self.manager.get_object(Author, author_id)
- if author is not None:
- self.song.authors.append(author)
+ author_song = AuthorSong()
+ author_song.author_id = item.data(QtCore.Qt.UserRole)[0]
+ author_song.author_type = item.data(QtCore.Qt.UserRole)[1]
+ self.song.authors_songs.append(author_song)
self.song.topics = []
for row in range(self.topics_list_view.count()):
item = self.topics_list_view.item(row)
diff --git a/openlp/plugins/songs/forms/songimportform.py b/openlp/plugins/songs/forms/songimportform.py
index 27f0d9343..21569a034 100644
--- a/openlp/plugins/songs/forms/songimportform.py
+++ b/openlp/plugins/songs/forms/songimportform.py
@@ -304,7 +304,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
"""
self.source_page.emit(QtCore.SIGNAL('completeChanged()'))
- def setDefaults(self):
+ def set_defaults(self):
"""
Set default form values for the song import wizard.
"""
diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py
index dc198d4b7..aa9fbc4c9 100644
--- a/openlp/plugins/songs/lib/__init__.py
+++ b/openlp/plugins/songs/lib/__init__.py
@@ -390,7 +390,7 @@ def clean_song(manager, song):
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one.
- if not song.authors:
+ if not song.authors and not song.authors_songs: # Need to check both relations
name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None:
diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py
index c3965e2ed..91649c951 100644
--- a/openlp/plugins/songs/lib/db.py
+++ b/openlp/plugins/songs/lib/db.py
@@ -35,19 +35,52 @@ import re
from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor
-from sqlalchemy.sql.expression import func
+from sqlalchemy.sql.expression import func, text
from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key
+from openlp.core.lib import translate
class Author(BaseModel):
"""
Author model
"""
+ def get_display_name(self, author_type=None):
+ if author_type:
+ return "%s (%s)" % (self.display_name, AuthorType.Types[author_type])
+ return self.display_name
+
+
+class AuthorSong(BaseModel):
+ """
+ Relationship between Authors and Songs (many to many).
+ Need to define this relationship table explicit to get access to the
+ Association Object (author_type).
+ http://docs.sqlalchemy.org/en/latest/orm/relationships.html#association-object
+ """
pass
+class AuthorType(object):
+ """
+ Enumeration for Author types.
+ They are defined by OpenLyrics: http://openlyrics.info/dataformat.html#authors
+
+ The 'words+music' type is not an official type, but is provided for convenience.
+ """
+ Words = 'words'
+ Music = 'music'
+ WordsAndMusic = 'words+music'
+ Translation = 'translation'
+ Types = {
+ Words: translate('OpenLP.Ui', 'Words'),
+ Music: translate('OpenLP.Ui', 'Music'),
+ WordsAndMusic: translate('OpenLP.Ui', 'Words and Music'),
+ Translation: translate('OpenLP.Ui', 'Translation')
+ }
+
+
class Book(BaseModel):
"""
Book model
@@ -67,6 +100,7 @@ class Song(BaseModel):
"""
Song model
"""
+
def __init__(self):
self.sort_key = []
@@ -120,6 +154,7 @@ def init_schema(url):
* author_id
* song_id
+ * author_type
**media_files Table**
* id
@@ -230,7 +265,8 @@ def init_schema(url):
authors_songs_table = Table(
'authors_songs', metadata,
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
- Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True)
+ Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
+ Column('author_type', types.String(), primary_key=True, nullable=False, server_default=text('""'))
)
# Definition of the "songs_topics" table
@@ -241,10 +277,15 @@ def init_schema(url):
)
mapper(Author, authors_table)
+ mapper(AuthorSong, authors_songs_table, properties={
+ 'author': relation(Author)
+ })
mapper(Book, song_books_table)
mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={
- 'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False),
+ # Use the authors_songs relation when you need access to the 'author_type' attribute.
+ 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"),
+ 'authors': relation(Author, secondary=authors_songs_table),
'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table)
diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py
index 314d1ffe7..32730ce3c 100644
--- a/openlp/plugins/songs/lib/mediaitem.py
+++ b/openlp/plugins/songs/lib/mediaitem.py
@@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
-from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
+from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@@ -234,8 +234,7 @@ class SongMediaItem(MediaManagerItem):
if song.temporary:
continue
author_list = [author.display_name for author in song.authors]
- song_title = str(song.title)
- song_detail = '%s (%s)' % (song_title, create_separated_list(author_list))
+ 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)
@@ -464,23 +463,53 @@ class SongMediaItem(MediaManagerItem):
def generate_footer(self, item, song):
"""
Generates the song footer based on a song and adds details to a service item.
- author_list is only required for initial song generation.
: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)
"""
- author_list = [str(author.display_name) for author in song.authors]
+ 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, author_list, song.copyright, str(song.ccli_number)
+ song.title, authors_all, song.copyright, str(song.ccli_number)
]
item.raw_footer = []
item.raw_footer.append(song.title)
- item.raw_footer.append(create_separated_list(author_list))
+ 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)))
item.raw_footer.append(song.copyright)
if Settings().value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem',
'CCLI License: ') + Settings().value('core/ccli number'))
- return author_list
+ return authors_all
def service_load(self, item):
"""
diff --git a/openlp/plugins/songs/lib/ui.py b/openlp/plugins/songs/lib/ui.py
index 14f4777c9..151b11b4b 100644
--- a/openlp/plugins/songs/lib/ui.py
+++ b/openlp/plugins/songs/lib/ui.py
@@ -40,7 +40,7 @@ class SongStrings(object):
# These strings should need a good reason to be retranslated elsewhere.
Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
- AuthorUnknown = 'Author Unknown' # Used to populate the database.
+ AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural')
diff --git a/openlp/plugins/songs/lib/upgrade.py b/openlp/plugins/songs/lib/upgrade.py
index adb7d8af5..580ae767d 100644
--- a/openlp/plugins/songs/lib/upgrade.py
+++ b/openlp/plugins/songs/lib/upgrade.py
@@ -32,14 +32,14 @@ backend for the Songs plugin
"""
import logging
-from sqlalchemy import Column, types
+from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import func, false, null, text
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
-__version__ = 3
+__version__ = 4
def upgrade_1(session, metadata):
@@ -97,3 +97,25 @@ def upgrade_3(session, metadata):
op.add_column('songs', Column('temporary', types.Boolean(), server_default=false()))
except OperationalError:
log.info('Upgrade 3 has already been run')
+
+
+def upgrade_4(session, metadata):
+ """
+ Version 4 upgrade.
+
+ This upgrade adds a column for author type to the authors_songs table
+ """
+ try:
+ # Since SQLite doesn't support changing the primary key of a table, we need to recreate the table
+ # and copy the old values
+ op = get_upgrade_op(session)
+ op.create_table('authors_songs_tmp',
+ Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
+ Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
+ Column('author_type', types.String(), primary_key=True,
+ nullable=False, server_default=text('""')))
+ op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs')
+ op.drop_table('authors_songs')
+ op.rename_table('authors_songs_tmp', 'authors_songs')
+ except OperationalError:
+ log.info('Upgrade 4 has already been run')
diff --git a/openlp/plugins/songs/lib/xml.py b/openlp/plugins/songs/lib/xml.py
index d516b5e02..68641a1d5 100644
--- a/openlp/plugins/songs/lib/xml.py
+++ b/openlp/plugins/songs/lib/xml.py
@@ -71,7 +71,7 @@ from lxml import etree, objectify
from openlp.core.common import translate
from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song
-from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
+from openlp.plugins.songs.lib.db import Author, AuthorSong, AuthorType, Book, Song, Topic
from openlp.core.utils import get_application_version
log = logging.getLogger(__name__)
@@ -166,7 +166,7 @@ class OpenLyrics(object):
supported by the :class:`OpenLyrics` class:
````
- OpenLP does not support the attribute *type* and *lang*.
+ OpenLP does not support the attribute *lang*.
````
This property is not supported.
@@ -269,10 +269,18 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower())
if song.ccli_number:
self._add_text_to_element('ccliNo', properties, song.ccli_number)
- if song.authors:
+ if song.authors_songs:
authors = etree.SubElement(properties, 'authors')
- for author in song.authors:
- self._add_text_to_element('author', authors, author.display_name)
+ for author_song in song.authors_songs:
+ element = self._add_text_to_element('author', authors, author_song.author.display_name)
+ if author_song.author_type:
+ # Handle the special case 'words+music': Need to create two separate authors for that
+ if author_song.author_type == AuthorType.WordsAndMusic:
+ element.set('type', AuthorType.Words)
+ element = self._add_text_to_element('author', authors, author_song.author.display_name)
+ element.set('type', AuthorType.Music)
+ else:
+ element.set('type', author_song.author_type)
book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id)
if book is not None:
book = book.name
@@ -501,16 +509,20 @@ class OpenLyrics(object):
if hasattr(properties, 'authors'):
for author in properties.authors.author:
display_name = self._text(author)
+ author_type = author.get('type', '')
if display_name:
- authors.append(display_name)
- for display_name in authors:
+ authors.append((display_name, author_type))
+ for (display_name, author_type) in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None:
# We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1]))
- song.authors.append(author)
+ author_song = AuthorSong()
+ author_song.author = author
+ author_song.author_type = author_type
+ song.authors_songs.append(author_song)
def _process_cclinumber(self, properties, song):
"""
diff --git a/resources/images/network_auth.png b/resources/images/network_auth.png
new file mode 100644
index 000000000..45e7a5c17
Binary files /dev/null and b/resources/images/network_auth.png differ
diff --git a/resources/images/network_server.png b/resources/images/network_server.png
new file mode 100644
index 000000000..25b95f3b0
Binary files /dev/null and b/resources/images/network_server.png differ
diff --git a/resources/images/network_ssl.png b/resources/images/network_ssl.png
new file mode 100644
index 000000000..1169de67a
Binary files /dev/null and b/resources/images/network_ssl.png differ
diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc
index 6af0e77a5..79036f08f 100644
--- a/resources/images/openlp-2.qrc
+++ b/resources/images/openlp-2.qrc
@@ -149,6 +149,11 @@
messagebox_info.png
messagebox_warning.png
+
+ network_server.png
+ network_ssl.png
+ network_auth.png
+
song_usage_active.png
song_usage_inactive.png
diff --git a/scripts/jenkins_script.py b/scripts/jenkins_script.py
index aaee9a71b..eeafbfe23 100644
--- a/scripts/jenkins_script.py
+++ b/scripts/jenkins_script.py
@@ -148,7 +148,7 @@ class JenkinsTrigger(object):
def get_repo_name():
"""
- This returns the name of branch of the wokring directory. For example it returns *lp:~googol/openlp/render*.
+ This returns the name of branch of the working directory. For example it returns *lp:~googol/openlp/render*.
"""
# Run the bzr command.
bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
@@ -198,7 +198,7 @@ def main():
jenkins_trigger = JenkinsTrigger(token)
try:
jenkins_trigger.trigger_build()
- except HTTPError as e:
+ except HTTPError:
print('Wrong token.')
return
# Open the browser before printing the output.
diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py
index 3120f48fa..ab7663a83 100644
--- a/tests/functional/openlp_core_lib/test_file_dialog.py
+++ b/tests/functional/openlp_core_lib/test_file_dialog.py
@@ -53,8 +53,8 @@ class TestFileDialog(TestCase):
self.mocked_os.rest()
self.mocked_qt_gui.reset()
- # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid
- # file names.
+ # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid file
+ # names.
self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [
'/Valid File', '/url%20encoded%20file%20%231', '/non-existing']
self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [
diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py
index 2b5f02483..308881c2e 100644
--- a/tests/functional/openlp_plugins/songs/test_mediaitem.py
+++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py
@@ -10,6 +10,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.common import Registry, Settings
from openlp.core.lib import ServiceItem
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
+from openlp.plugins.songs.lib.db import AuthorType
from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin
@@ -45,10 +46,12 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item
mock_song = MagicMock()
mock_song.title = 'My Song'
+ mock_song.authors_songs = []
mock_author = MagicMock()
mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
@@ -56,7 +59,7 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'Written by: my author', 'My copyright'],
'The array should be returned correctly with a song, one author and copyright')
self.assertEqual(author_list, ['my author'],
'The author list should be returned correctly with one author')
@@ -68,13 +71,25 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item
mock_song = MagicMock()
mock_song.title = 'My Song'
+ mock_song.authors_songs = []
mock_author = MagicMock()
mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Music
+ mock_song.authors_songs.append(mock_author_song)
mock_author = MagicMock()
mock_author.display_name = 'another author'
- mock_song.authors.append(mock_author)
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Words
+ mock_song.authors_songs.append(mock_author_song)
+ mock_author = MagicMock()
+ mock_author.display_name = 'translator'
+ mock_author_song = MagicMock()
+ mock_author_song.author = mock_author
+ mock_author_song.author_type = AuthorType.Translation
+ mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
@@ -82,22 +97,19 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author and another author', 'My copyright'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'Words: another author', 'Music: my author',
+ 'Translation: translator', 'My copyright'],
'The array should be returned correctly with a song, two authors and copyright')
- self.assertEqual(author_list, ['my author', 'another author'],
+ self.assertEqual(author_list, ['another author', 'my author', 'translator'],
'The author list should be returned correctly with two authors')
def build_song_footer_base_ccli_test(self):
"""
- Test build songs footer with basic song and two authors
+ Test build songs footer with basic song and a CCLI number
"""
# GIVEN: A Song and a Service Item and a configured CCLI license
mock_song = MagicMock()
mock_song.title = 'My Song'
- mock_author = MagicMock()
- mock_author.display_name = 'my author'
- mock_song.authors = []
- mock_song.authors.append(mock_author)
mock_song.copyright = 'My copyright'
service_item = ServiceItem(None)
Settings().setValue('core/ccli number', '1234')
@@ -106,7 +118,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 1234'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 1234'],
'The array should be returned correctly with a song, an author, copyright and ccli')
# WHEN: I amend the CCLI value
@@ -114,5 +126,5 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song)
# THEN: I would get an amended footer string
- self.assertEqual(service_item.raw_footer, ['My Song', 'my author', 'My copyright', 'CCLI License: 4321'],
+ self.assertEqual(service_item.raw_footer, ['My Song', 'My copyright', 'CCLI License: 4321'],
'The array should be returned correctly with a song, an author, copyright and amended ccli')