This commit is contained in:
Andreas Preikschat 2014-04-29 13:04:32 +02:00
commit b73d8b088c
29 changed files with 312 additions and 114 deletions

View File

@ -6,6 +6,8 @@
*.ropeproject *.ropeproject
*.e4* *.e4*
.eric4project .eric4project
.komodotools
*.komodoproject
list list
openlp.org 2.0.e4* openlp.org 2.0.e4*
documentation/build/html documentation/build/html

View File

@ -194,6 +194,7 @@ class Manager(object):
db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod) db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
except (SQLAlchemyError, DBAPIError): except (SQLAlchemyError, DBAPIError):
log.exception('Error loading database: %s', self.db_url) log.exception('Error loading database: %s', self.db_url)
return
if db_ver > up_ver: if db_ver > up_ver:
critical_error_message_box( critical_error_message_box(
translate('OpenLP.Manager', 'Database Error'), translate('OpenLP.Manager', 'Database Error'),
@ -215,7 +216,7 @@ class Manager(object):
Save an object to the database Save an object to the database
:param object_instance: The object to save :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): for try_count in range(3):
try: try:

View File

@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
""" """
Run the wizard. Run the wizard.
""" """
self.setDefaults() self.set_defaults()
return QtGui.QWizard.exec_(self) return QtGui.QWizard.exec_(self)
def setDefaults(self): def set_defaults(self):
""" """
Set up display at start of theme edit. Set up display at start of theme edit.
""" """

View File

@ -30,7 +30,6 @@
The actual plugin view form The actual plugin view form
""" """
import logging import logging
import os
from PyQt4 import QtGui from PyQt4 import QtGui

View File

@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
self.footer_font_combo_box.activated.connect(self.update_theme) self.footer_font_combo_box.activated.connect(self.update_theme)
self.footer_size_spin_box.valueChanged.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. 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) log.debug('Editing theme %s' % self.theme.theme_name)
self.temp_background_filename = '' self.temp_background_filename = ''
self.update_theme_allowed = False self.update_theme_allowed = False
self.setDefaults() self.set_defaults()
self.update_theme_allowed = True self.update_theme_allowed = True
self.theme_name_label.setVisible(not edit) self.theme_name_label.setVisible(not edit)
self.theme_name_edit.setVisible(not edit) self.theme_name_edit.setVisible(not edit)

View File

@ -197,7 +197,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
""" """
Run the wizard. Run the wizard.
""" """
self.setDefaults() self.set_defaults()
return QtGui.QWizard.exec_(self) return QtGui.QWizard.exec_(self)
def reject(self): def reject(self):

View File

@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard):
self.license_details_page.registerField('license_copyright', self.copyright_edit) self.license_details_page.registerField('license_copyright', self.copyright_edit)
self.license_details_page.registerField('license_permissions', self.permissions_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. Set default values for the wizard pages.
""" """

View File

@ -307,7 +307,7 @@ class BibleUpgradeForm(OpenLPWizard):
if self.currentPage() == self.progress_page: if self.currentPage() == self.progress_page:
return True return True
def setDefaults(self): def set_defaults(self):
""" """
Set default values for the wizard pages. Set default values for the wizard pages.
""" """

View File

@ -149,11 +149,11 @@ class HttpRouter(RegistryProperties):
""" """
Initialise the router stack and any other variables. 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: try:
self.auth = base64.b64encode(authcode) self.auth = base64.b64encode(auth_code)
except TypeError: except TypeError:
self.auth = base64.b64encode(authcode.encode()).decode() self.auth = base64.b64encode(auth_code.encode()).decode()
self.routes = [ self.routes = [
('^/$', {'function': self.serve_file, 'secure': False}), ('^/$', {'function': self.serve_file, 'secure': False}),
('^/(stage)$', {'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 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 Returns the extension and the content_type
""" """
content_type = 'text/plain'
ext = os.path.splitext(file_name)[1] ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain') content_type = FILE_TYPES.get(ext, 'text/plain')
return ext, content_type return ext, content_type
@ -439,7 +438,7 @@ class HttpRouter(RegistryProperties):
if plugin.status == PluginStatus.Active: if plugin.status == PluginStatus.Active:
try: try:
text = json.loads(self.request_data)['request']['text'] text = json.loads(self.request_data)['request']['text']
except KeyError as ValueError: except KeyError:
return self.do_http_error() return self.do_http_error()
text = urllib.parse.unquote(text) text = urllib.parse.unquote(text)
self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text]) self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text])
@ -453,6 +452,7 @@ class HttpRouter(RegistryProperties):
""" """
Perform an action on the slide controller. Perform an action on the slide controller.
""" """
log.debug("controller_text var = %s" % var)
current_item = self.live_controller.service_item current_item = self.live_controller.service_item
data = [] data = []
if current_item: if current_item:
@ -488,7 +488,7 @@ class HttpRouter(RegistryProperties):
if self.request_data: if self.request_data:
try: try:
data = json.loads(self.request_data)['request']['id'] data = json.loads(self.request_data)['request']['id']
except KeyError as ValueError: except KeyError:
return self.do_http_error() return self.do_http_error()
log.info(data) log.info(data)
# This slot expects an int within a list. # This slot expects an int within a list.
@ -547,7 +547,7 @@ class HttpRouter(RegistryProperties):
""" """
try: try:
text = json.loads(self.request_data)['request']['text'] text = json.loads(self.request_data)['request']['text']
except KeyError as ValueError: except KeyError:
return self.do_http_error() return self.do_http_error()
text = urllib.parse.unquote(text) text = urllib.parse.unquote(text)
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) 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``. Go live on an item of type ``plugin``.
""" """
try: try:
id = json.loads(self.request_data)['request']['id'] request_id = json.loads(self.request_data)['request']['id']
except KeyError as ValueError: except KeyError:
return self.do_http_error() return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item: 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() return self.do_http_success()
def add_to_service(self, plugin_name): 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. Add item of type ``plugin_name`` to the end of the service.
""" """
try: try:
id = json.loads(self.request_data)['request']['id'] request_id = json.loads(self.request_data)['request']['id']
except KeyError as ValueError: except KeyError:
return self.do_http_error() return self.do_http_error()
plugin = self.plugin_manager.get_plugin_by_name(plugin_name) plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
if plugin.status == PluginStatus.Active and plugin.media_item: 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]) plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
self.do_http_success() self.do_http_success()

View File

@ -40,7 +40,7 @@ import time
from PyQt4 import QtCore 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 from openlp.plugins.remotes.lib import HttpRouter
@ -94,13 +94,18 @@ class HttpThread(QtCore.QThread):
""" """
self.http_server.start_server() 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): def __init__(self):
""" """
Initialise the http server, and start the server of the correct type http / https 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.settings_section = 'remotes'
self.http_thread = HttpThread(self) self.http_thread = HttpThread(self)
self.http_thread.start() self.http_thread.start()
@ -110,32 +115,49 @@ class OpenLPServer():
Start the correct server and save the handler Start the correct server and save the handler
""" """
address = Settings().value(self.settings_section + '/ip address') 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') port = Settings().value(self.settings_section + '/https port')
self.httpd = HTTPSServer((address, port), CustomHandler) self.port = port
log.debug('Started ssl httpd...') self.start_server_instance(address, port, HTTPSServer)
else: else:
port = Settings().value(self.settings_section + '/port') port = Settings().value(self.settings_section + '/port')
loop = 1 self.port = port
while loop < 3: self.start_server_instance(address, port, ThreadingHTTPServer)
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...')
if hasattr(self, 'httpd') and self.httpd: if hasattr(self, 'httpd') and self.httpd:
self.httpd.serve_forever() self.httpd.serve_forever()
else: else:
log.debug('Failed to start server') 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): def stop_server(self):
""" """
Stop the server Stop the server
""" """
self.http_thread.exit(0) if self.http_thread.isRunning():
self.http_thread.stop()
self.httpd = None self.httpd = None
log.debug('Stopped the server.') log.debug('Stopped the server.')

View File

@ -32,7 +32,7 @@ import os.path
from PyQt4 import QtCore, QtGui, QtNetwork from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.common import AppLocation, Settings, translate 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' ZERO_URL = '0.0.0.0'
@ -234,6 +234,7 @@ class RemoteTab(SettingsTab):
""" """
Load the configuration and update the server configuration if necessary 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.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + '/https 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')) 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 + '/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 port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + '/https enabled') != \ Settings().value(self.settings_section + '/https enabled') != \
self.https_settings_group_box.isChecked() or \ self.https_settings_group_box.isChecked():
Settings().value(self.settings_section + '/authentication enabled') != \
self.user_login_group_box.isChecked():
self.settings_form.register_post_process('remotes_config_updated') 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 + '/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + '/https port', self.https_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 + '/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + '/user id', self.user_id.text()) Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text()) Settings().setValue(self.settings_section + '/password', self.password.text())
self.generate_icon()
def on_twelve_hour_check_box_changed(self, check_state): 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 Invert the HTTP group box based on Https group settings
""" """
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked()) 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()

View File

@ -28,7 +28,8 @@
############################################################################### ###############################################################################
import logging import logging
import time
from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
@ -67,6 +68,21 @@ class RemotesPlugin(Plugin):
log.debug('initialise') log.debug('initialise')
super(RemotesPlugin, self).initialise() super(RemotesPlugin, self).initialise()
self.server = OpenLPServer() 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): def finalise(self):
""" """
@ -104,9 +120,11 @@ class RemotesPlugin(Plugin):
def config_update(self): 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') log.debug('remote config changed')
self.finalise() QtGui.QMessageBox.information(self.main_window,
time.sleep(0.5) translate('RemotePlugin', 'Server Config Change'),
self.initialise() translate('RemotePlugin', 'Server configuration changes will require a restart '
'to take effect.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))

View File

@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True self.break_search = True
self.plugin.media_item.on_search_text_button_clicked() 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. Set default form values for the song import wizard.
""" """

View File

@ -118,13 +118,18 @@ class Ui_EditSongDialog(object):
self.authors_group_box.setObjectName('authors_group_box') self.authors_group_box.setObjectName('authors_group_box')
self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box) self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box)
self.authors_layout.setObjectName('authors_layout') 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_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.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box')
self.author_add_layout.addWidget(self.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 = QtGui.QPushButton(self.authors_group_box)
self.author_add_button.setObjectName('author_add_button') 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_layout.addLayout(self.author_add_layout)
self.authors_list_view = QtGui.QListWidget(self.authors_group_box) self.authors_list_view = QtGui.QListWidget(self.authors_group_box)
self.authors_list_view.setAlternatingRowColors(True) self.authors_list_view.setAlternatingRowColors(True)
@ -330,7 +335,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.') translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> 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. 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 = QtGui.QComboBox(parent)
combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength) combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) 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.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name) combo_box.setObjectName(name)
return combo_box return combo_box

View File

@ -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 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.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 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.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.xml import SongXML
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog 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) combo.setItemData(row, obj.id)
set_case_insensitive_completer(cache, combo) 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. Add an author to the author list.
""" """
author_item = QtGui.QListWidgetItem(str(author.display_name)) author_item = QtGui.QListWidgetItem(author.get_display_name(author_type))
author_item.setData(QtCore.Qt.UserRole, author.id) author_item.setData(QtCore.Qt.UserRole, (author.id, author_type))
self.authors_list_view.addItem(author_item) self.authors_list_view.addItem(author_item)
def _extract_verse_order(self, verse_order): 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: if self.authors_list_view.count() == 0:
self.song_tab_widget.setCurrentIndex(1) self.song_tab_widget.setCurrentIndex(1)
self.authors_list_view.setFocus() self.authors_list_view.setFocus()
critical_error_message_box( critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.')) 'You need to have an author for this song.'))
return False return False
if self.verse_order_edit.text(): if self.verse_order_edit.text():
result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount()) 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) self.authors.append(author.display_name)
set_case_insensitive_completer(self.authors, self.authors_combo_box) 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): def load_topics(self):
""" """
Load the topics into the combobox. Load the topics into the combobox.
@ -454,10 +463,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.tag_rows() self.tag_rows()
# clear the results # clear the results
self.authors_list_view.clear() self.authors_list_view.clear()
for author in self.song.authors: for author_song in self.song.authors_songs:
author_name = QtGui.QListWidgetItem(str(author.display_name)) self._add_author_to_list(author_song.author, author_song.author_type)
author_name.setData(QtCore.Qt.UserRole, author.id)
self.authors_list_view.addItem(author_name)
# clear the results # clear the results
self.topics_list_view.clear() self.topics_list_view.clear()
for topic in self.song.topics: for topic in self.song.topics:
@ -496,6 +503,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
""" """
item = int(self.authors_combo_box.currentIndex()) item = int(self.authors_combo_box.currentIndex())
text = self.authors_combo_box.currentText().strip(' \r\n\t') 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 # This if statement is for OS X, which doesn't seem to work well with
# the QCompleter auto-completion class. See bug #812628. # the QCompleter auto-completion class. See bug #812628.
if text in self.authors: 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], author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
display_name=text) display_name=text)
self.manager.save_object(author) self.manager.save_object(author)
self._add_author_to_list(author) self._add_author_to_list(author, author_type)
self.load_authors() self.load_authors()
self.authors_combo_box.setCurrentIndex(0) self.authors_combo_box.setCurrentIndex(0)
else: else:
@ -521,11 +529,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0: elif item > 0:
item_id = (self.authors_combo_box.itemData(item)) item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id) 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( critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.')) message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else: else:
self._add_author_to_list(author) self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0) self.authors_combo_box.setCurrentIndex(0)
else: else:
QtGui.QMessageBox.warning( QtGui.QMessageBox.warning(
@ -905,13 +913,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
else: else:
self.song.theme_name = None self.song.theme_name = None
self._process_lyrics() self._process_lyrics()
self.song.authors = [] self.song.authors_songs = []
for row in range(self.authors_list_view.count()): for row in range(self.authors_list_view.count()):
item = self.authors_list_view.item(row) item = self.authors_list_view.item(row)
author_id = (item.data(QtCore.Qt.UserRole)) author_song = AuthorSong()
author = self.manager.get_object(Author, author_id) author_song.author_id = item.data(QtCore.Qt.UserRole)[0]
if author is not None: author_song.author_type = item.data(QtCore.Qt.UserRole)[1]
self.song.authors.append(author) self.song.authors_songs.append(author_song)
self.song.topics = [] self.song.topics = []
for row in range(self.topics_list_view.count()): for row in range(self.topics_list_view.count()):
item = self.topics_list_view.item(row) item = self.topics_list_view.item(row)

View File

@ -304,7 +304,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
""" """
self.source_page.emit(QtCore.SIGNAL('completeChanged()')) self.source_page.emit(QtCore.SIGNAL('completeChanged()'))
def setDefaults(self): def set_defaults(self):
""" """
Set default form values for the song import wizard. Set default form values for the song import wizard.
""" """

View File

@ -390,7 +390,7 @@ def clean_song(manager, song):
verses = SongXML().get_verses(song.lyrics) verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses]) song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one. # 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 name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name) author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None: if author is None:

View File

@ -35,19 +35,52 @@ import re
from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor 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.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key from openlp.core.utils import get_natural_key
from openlp.core.lib import translate
class Author(BaseModel): class Author(BaseModel):
""" """
Author model 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 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): class Book(BaseModel):
""" """
Book model Book model
@ -67,6 +100,7 @@ class Song(BaseModel):
""" """
Song model Song model
""" """
def __init__(self): def __init__(self):
self.sort_key = [] self.sort_key = []
@ -120,6 +154,7 @@ def init_schema(url):
* author_id * author_id
* song_id * song_id
* author_type
**media_files Table** **media_files Table**
* id * id
@ -230,7 +265,8 @@ def init_schema(url):
authors_songs_table = Table( authors_songs_table = Table(
'authors_songs', metadata, 'authors_songs', metadata,
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True), 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 # Definition of the "songs_topics" table
@ -241,10 +277,15 @@ def init_schema(url):
) )
mapper(Author, authors_table) mapper(Author, authors_table)
mapper(AuthorSong, authors_songs_table, properties={
'author': relation(Author)
})
mapper(Book, song_books_table) mapper(Book, song_books_table)
mapper(MediaFile, media_files_table) mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={ 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'), 'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table) 'topics': relation(Topic, backref='songs', secondary=songs_topics_table)

View File

@ -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.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song 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.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@ -234,8 +234,7 @@ class SongMediaItem(MediaManagerItem):
if song.temporary: if song.temporary:
continue continue
author_list = [author.display_name for author in song.authors] 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)) if author_list else song.title
song_detail = '%s (%s)' % (song_title, create_separated_list(author_list))
song_name = QtGui.QListWidgetItem(song_detail) song_name = QtGui.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id) song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name) self.list_view.addItem(song_name)
@ -464,23 +463,53 @@ class SongMediaItem(MediaManagerItem):
def generate_footer(self, item, song): def generate_footer(self, item, song):
""" """
Generates the song footer based on a song and adds details to a service item. 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 item: The service item to be amended
:param song: The song to be used to generate the footer :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 = [ 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 = []
item.raw_footer.append(song.title) 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) item.raw_footer.append(song.copyright)
if Settings().value('core/ccli number'): if Settings().value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem', item.raw_footer.append(translate('SongsPlugin.MediaItem',
'CCLI License: ') + Settings().value('core/ccli number')) 'CCLI License: ') + Settings().value('core/ccli number'))
return author_list return authors_all
def service_load(self, item): def service_load(self, item):
""" """

View File

@ -40,7 +40,7 @@ class SongStrings(object):
# These strings should need a good reason to be retranslated elsewhere. # These strings should need a good reason to be retranslated elsewhere.
Author = translate('OpenLP.Ui', 'Author', 'Singular') Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural') 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.') CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular') SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural') SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural')

View File

@ -32,14 +32,14 @@ backend for the Songs plugin
""" """
import logging import logging
from sqlalchemy import Column, types from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import func, false, null, text from sqlalchemy.sql.expression import func, false, null, text
from openlp.core.lib.db import get_upgrade_op from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
__version__ = 3 __version__ = 4
def upgrade_1(session, metadata): 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())) op.add_column('songs', Column('temporary', types.Boolean(), server_default=false()))
except OperationalError: except OperationalError:
log.info('Upgrade 3 has already been run') 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')

View File

@ -71,7 +71,7 @@ from lxml import etree, objectify
from openlp.core.common import translate from openlp.core.common import translate
from openlp.core.lib import FormattingTags from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song 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 from openlp.core.utils import get_application_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -166,7 +166,7 @@ class OpenLyrics(object):
supported by the :class:`OpenLyrics` class: supported by the :class:`OpenLyrics` class:
``<authors>`` ``<authors>``
OpenLP does not support the attribute *type* and *lang*. OpenLP does not support the attribute *lang*.
``<chord>`` ``<chord>``
This property is not supported. This property is not supported.
@ -269,10 +269,18 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower()) 'verseOrder', properties, song.verse_order.lower())
if song.ccli_number: if song.ccli_number:
self._add_text_to_element('ccliNo', properties, 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') authors = etree.SubElement(properties, 'authors')
for author in song.authors: for author_song in song.authors_songs:
self._add_text_to_element('author', authors, author.display_name) 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) book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id)
if book is not None: if book is not None:
book = book.name book = book.name
@ -501,16 +509,20 @@ class OpenLyrics(object):
if hasattr(properties, 'authors'): if hasattr(properties, 'authors'):
for author in properties.authors.author: for author in properties.authors.author:
display_name = self._text(author) display_name = self._text(author)
author_type = author.get('type', '')
if display_name: if display_name:
authors.append(display_name) authors.append((display_name, author_type))
for display_name in authors: for (display_name, author_type) in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name) author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None: if author is None:
# We need to create a new author, as the author does not exist. # We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name, author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1], last_name=display_name.split(' ')[-1],
first_name=' '.join(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): def _process_cclinumber(self, properties, song):
""" """

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

View File

@ -149,6 +149,11 @@
<file>messagebox_info.png</file> <file>messagebox_info.png</file>
<file>messagebox_warning.png</file> <file>messagebox_warning.png</file>
</qresource> </qresource>
<qresource prefix="remote">
<file>network_server.png</file>
<file>network_ssl.png</file>
<file>network_auth.png</file>
</qresource>
<qresource prefix="songusage"> <qresource prefix="songusage">
<file>song_usage_active.png</file> <file>song_usage_active.png</file>
<file>song_usage_inactive.png</file> <file>song_usage_inactive.png</file>

View File

@ -148,7 +148,7 @@ class JenkinsTrigger(object):
def get_repo_name(): 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. # Run the bzr command.
bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE) bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
@ -198,7 +198,7 @@ def main():
jenkins_trigger = JenkinsTrigger(token) jenkins_trigger = JenkinsTrigger(token)
try: try:
jenkins_trigger.trigger_build() jenkins_trigger.trigger_build()
except HTTPError as e: except HTTPError:
print('Wrong token.') print('Wrong token.')
return return
# Open the browser before printing the output. # Open the browser before printing the output.

View File

@ -53,8 +53,8 @@ class TestFileDialog(TestCase):
self.mocked_os.rest() self.mocked_os.rest()
self.mocked_qt_gui.reset() self.mocked_qt_gui.reset()
# GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid # GIVEN: A List of known values as a return value from QFileDialog.getOpenFileNames and a list of valid file
# file names. # names.
self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [ self.mocked_qt_gui.QFileDialog.getOpenFileNames.return_value = [
'/Valid File', '/url%20encoded%20file%20%231', '/non-existing'] '/Valid File', '/url%20encoded%20file%20%231', '/non-existing']
self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [ self.mocked_os.path.exists.side_effect = lambda file_name: file_name in [

View File

@ -10,6 +10,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.common import Registry, Settings from openlp.core.common import Registry, Settings
from openlp.core.lib import ServiceItem from openlp.core.lib import ServiceItem
from openlp.plugins.songs.lib.mediaitem import SongMediaItem from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.lib.db import AuthorType
from tests.functional import patch, MagicMock from tests.functional import patch, MagicMock
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin
@ -45,10 +46,12 @@ class TestMediaItem(TestCase, TestMixin):
# GIVEN: A Song and a Service Item # GIVEN: A Song and a Service Item
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' mock_song.title = 'My Song'
mock_song.authors_songs = []
mock_author = MagicMock() mock_author = MagicMock()
mock_author.display_name = 'my author' mock_author.display_name = 'my author'
mock_song.authors = [] mock_author_song = MagicMock()
mock_song.authors.append(mock_author) mock_author_song.author = mock_author
mock_song.authors_songs.append(mock_author_song)
mock_song.copyright = 'My copyright' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
@ -56,7 +59,7 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song) author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # 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') 'The array should be returned correctly with a song, one author and copyright')
self.assertEqual(author_list, ['my author'], self.assertEqual(author_list, ['my author'],
'The author list should be returned correctly with one 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 # GIVEN: A Song and a Service Item
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' mock_song.title = 'My Song'
mock_song.authors_songs = []
mock_author = MagicMock() mock_author = MagicMock()
mock_author.display_name = 'my author' mock_author.display_name = 'my author'
mock_song.authors = [] mock_author_song = MagicMock()
mock_song.authors.append(mock_author) 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 = MagicMock()
mock_author.display_name = 'another author' 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' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
@ -82,22 +97,19 @@ class TestMediaItem(TestCase, TestMixin):
author_list = self.media_item.generate_footer(service_item, mock_song) author_list = self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # 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') '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') 'The author list should be returned correctly with two authors')
def build_song_footer_base_ccli_test(self): 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 # GIVEN: A Song and a Service Item and a configured CCLI license
mock_song = MagicMock() mock_song = MagicMock()
mock_song.title = 'My Song' 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' mock_song.copyright = 'My copyright'
service_item = ServiceItem(None) service_item = ServiceItem(None)
Settings().setValue('core/ccli number', '1234') Settings().setValue('core/ccli number', '1234')
@ -106,7 +118,7 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song) self.media_item.generate_footer(service_item, mock_song)
# THEN: I get the following Array returned # 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') 'The array should be returned correctly with a song, an author, copyright and ccli')
# WHEN: I amend the CCLI value # WHEN: I amend the CCLI value
@ -114,5 +126,5 @@ class TestMediaItem(TestCase, TestMixin):
self.media_item.generate_footer(service_item, mock_song) self.media_item.generate_footer(service_item, mock_song)
# THEN: I would get an amended footer string # 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') 'The array should be returned correctly with a song, an author, copyright and amended ccli')