forked from openlp/openlp
Merge from parent
This commit is contained in:
commit
a56f00ddc0
@ -6,6 +6,8 @@
|
||||
*.ropeproject
|
||||
*.e4*
|
||||
.eric4project
|
||||
.komodotools
|
||||
*.komodoproject
|
||||
list
|
||||
openlp.org 2.0.e4*
|
||||
documentation/build/html
|
||||
|
@ -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:
|
||||
|
@ -248,6 +248,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
elif item.is_capable(ItemCapabilities.CanSoftBreak):
|
||||
pages = []
|
||||
if '[---]' in text:
|
||||
# Remove two or more option slide breaks next to each other (causing infinite loop).
|
||||
while '\n[---]\n[---]\n' in text:
|
||||
text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
|
||||
while True:
|
||||
slides = text.split('\n[---]\n', 2)
|
||||
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
|
||||
@ -392,7 +395,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
off when displayed.
|
||||
|
||||
:param lines: The text to be fitted on the slide split into lines.
|
||||
:param line_end: The text added after each line. Either ``u' '`` or ``u'<br>``.
|
||||
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
|
||||
"""
|
||||
formatted = []
|
||||
previous_html = ''
|
||||
@ -416,7 +419,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
processed word by word. This is sometimes need for **bible** verses.
|
||||
|
||||
:param lines: The text to be fitted on the slide split into lines.
|
||||
:param line_end: The text added after each line. Either ``u' '`` or ``u'<br>``. This is needed for **bibles**.
|
||||
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
|
||||
"""
|
||||
formatted = []
|
||||
previous_html = ''
|
||||
@ -453,7 +456,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
"""
|
||||
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
|
||||
|
||||
(u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong><span style="-webkit-text-fill-color:red">')
|
||||
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
|
||||
|
||||
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
|
||||
formatting tags and the third unicode string the html opening formatting tags.
|
||||
@ -500,8 +503,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
The text contains html.
|
||||
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
|
||||
The elements can contain formatting tags.
|
||||
:param separator: The separator for the elements. For lines this is ``u'<br>'`` and for words this is ``u' '``.
|
||||
:param line_end: The text added after each "element line". Either ``u' '`` or ``u'<br>``. This is needed for
|
||||
:param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
|
||||
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
|
||||
bibles.
|
||||
"""
|
||||
smallest_index = 0
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -1334,7 +1334,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
if self.copy_data:
|
||||
log.info('Copying data to new path')
|
||||
try:
|
||||
self.showStatusMessage(
|
||||
self.show_status_message(
|
||||
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
|
||||
'- Please wait for copy to finish').replace('%s', self.new_data_path))
|
||||
dir_util.copy_tree(old_data_path, self.new_data_path)
|
||||
@ -1364,8 +1364,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
args = []
|
||||
for a in self.arguments:
|
||||
args.extend([a])
|
||||
for arg in args:
|
||||
filename = arg
|
||||
for filename in args:
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, sys.getfilesystemencoding())
|
||||
if filename.endswith(('.osz', '.oszl')):
|
||||
|
@ -30,7 +30,6 @@
|
||||
The actual plugin view form
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt4 import QtGui
|
||||
|
||||
|
@ -244,10 +244,10 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||
self.primary_push_button.setChecked(False)
|
||||
self.alternate_push_button.setChecked(False)
|
||||
else:
|
||||
if action.defaultShortcuts:
|
||||
primary_label_text = action.defaultShortcuts[0].toString()
|
||||
if len(action.defaultShortcuts) == 2:
|
||||
alternate_label_text = action.defaultShortcuts[1].toString()
|
||||
if action.default_shortcuts:
|
||||
primary_label_text = action.default_shortcuts[0].toString()
|
||||
if len(action.default_shortcuts) == 2:
|
||||
alternate_label_text = action.default_shortcuts[1].toString()
|
||||
shortcuts = self._action_shortcuts(action)
|
||||
# We do not want to loose pending changes, that is why we have to keep the text when, this function has not
|
||||
# been triggered by a signal.
|
||||
@ -292,7 +292,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||
self._adjust_button(self.alternate_push_button, False, text='')
|
||||
for category in self.action_list.categories:
|
||||
for action in category.actions:
|
||||
self.changed_actions[action] = action.defaultShortcuts
|
||||
self.changed_actions[action] = action.default_shortcuts
|
||||
self.refresh_shortcut_list()
|
||||
|
||||
def on_default_radio_button_clicked(self, toggled):
|
||||
@ -306,7 +306,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||
if action is None:
|
||||
return
|
||||
temp_shortcuts = self._action_shortcuts(action)
|
||||
self.changed_actions[action] = action.defaultShortcuts
|
||||
self.changed_actions[action] = action.default_shortcuts
|
||||
self.refresh_shortcut_list()
|
||||
primary_button_text = ''
|
||||
alternate_button_text = ''
|
||||
@ -357,8 +357,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||
return
|
||||
shortcuts = self._action_shortcuts(action)
|
||||
new_shortcuts = []
|
||||
if action.defaultShortcuts:
|
||||
new_shortcuts.append(action.defaultShortcuts[0])
|
||||
if action.default_shortcuts:
|
||||
new_shortcuts.append(action.default_shortcuts[0])
|
||||
# We have to check if the primary default shortcut is available. But we only have to check, if the action
|
||||
# has a default primary shortcut (an "empty" shortcut is always valid and if the action does not have a
|
||||
# default primary shortcut, then the alternative shortcut (not the default one) will become primary
|
||||
@ -383,8 +383,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||
new_shortcuts = []
|
||||
if shortcuts:
|
||||
new_shortcuts.append(shortcuts[0])
|
||||
if len(action.defaultShortcuts) == 2:
|
||||
new_shortcuts.append(action.defaultShortcuts[1])
|
||||
if len(action.default_shortcuts) == 2:
|
||||
new_shortcuts.append(action.default_shortcuts[1])
|
||||
if len(new_shortcuts) == 2:
|
||||
if not self._validiate_shortcut(action, new_shortcuts[1]):
|
||||
return
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -65,20 +65,14 @@ class CategoryActionList(object):
|
||||
self.index = 0
|
||||
self.actions = []
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Implement the __getitem__() method to make this class a dictionary type
|
||||
"""
|
||||
for weight, action in self.actions:
|
||||
if action.text() == key:
|
||||
return action
|
||||
raise KeyError('Action "%s" does not exist.' % key)
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, key):
|
||||
"""
|
||||
Implement the __contains__() method to make this class a dictionary type
|
||||
"""
|
||||
return item in self
|
||||
for weight, action in self.actions:
|
||||
if action == key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
@ -103,23 +97,14 @@ class CategoryActionList(object):
|
||||
self.index += 1
|
||||
return self.actions[self.index - 1][1]
|
||||
|
||||
def has_key(self, key):
|
||||
"""
|
||||
Implement the has_key() method to make this class a dictionary type
|
||||
"""
|
||||
for weight, action in self.actions:
|
||||
if action.text() == key:
|
||||
return True
|
||||
return False
|
||||
|
||||
def append(self, name):
|
||||
def append(self, action):
|
||||
"""
|
||||
Append an action
|
||||
"""
|
||||
weight = 0
|
||||
if self.actions:
|
||||
weight = self.actions[-1][0] + 1
|
||||
self.add(name, weight)
|
||||
self.add(action, weight)
|
||||
|
||||
def add(self, action, weight=0):
|
||||
"""
|
||||
@ -128,14 +113,15 @@ class CategoryActionList(object):
|
||||
self.actions.append((weight, action))
|
||||
self.actions.sort(key=lambda act: act[0])
|
||||
|
||||
def remove(self, remove_action):
|
||||
def remove(self, action):
|
||||
"""
|
||||
Remove an action
|
||||
"""
|
||||
for action in self.actions:
|
||||
if action[1] == remove_action:
|
||||
self.actions.remove(action)
|
||||
for item in self.actions:
|
||||
if item[1] == action:
|
||||
self.actions.remove(item)
|
||||
return
|
||||
raise ValueError('Action "%s" does not exist.' % action)
|
||||
|
||||
|
||||
class CategoryList(object):
|
||||
@ -184,9 +170,9 @@ class CategoryList(object):
|
||||
self.index += 1
|
||||
return self.categories[self.index - 1]
|
||||
|
||||
def has_key(self, key):
|
||||
def __contains__(self, key):
|
||||
"""
|
||||
Implement the has_key() method to make this class like a dictionary
|
||||
Implement the __contains__() method to make this class like a dictionary
|
||||
"""
|
||||
for category in self.categories:
|
||||
if category.name == key:
|
||||
@ -200,10 +186,7 @@ class CategoryList(object):
|
||||
weight = 0
|
||||
if self.categories:
|
||||
weight = self.categories[-1].weight + 1
|
||||
if actions:
|
||||
self.add(name, weight, actions)
|
||||
else:
|
||||
self.add(name, weight)
|
||||
self.add(name, weight, actions)
|
||||
|
||||
def add(self, name, weight=0, actions=None):
|
||||
"""
|
||||
@ -226,6 +209,8 @@ class CategoryList(object):
|
||||
for category in self.categories:
|
||||
if category.name == name:
|
||||
self.categories.remove(category)
|
||||
return
|
||||
raise ValueError('Category "%s" does not exist.' % name)
|
||||
|
||||
|
||||
class ActionList(object):
|
||||
@ -270,7 +255,7 @@ class ActionList(object):
|
||||
settings = Settings()
|
||||
settings.beginGroup('shortcuts')
|
||||
# Get the default shortcut from the config.
|
||||
action.defaultShortcuts = settings.get_default_value(action.objectName())
|
||||
action.default_shortcuts = settings.get_default_value(action.objectName())
|
||||
if weight is None:
|
||||
self.categories[category].actions.append(action)
|
||||
else:
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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.')
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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', '<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.
|
||||
|
||||
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -34,13 +34,13 @@ EasyWorship song databases into the current installation database.
|
||||
import os
|
||||
import struct
|
||||
import re
|
||||
import zlib
|
||||
|
||||
from openlp.core.lib import translate
|
||||
from openlp.plugins.songs.lib import VerseType
|
||||
from openlp.plugins.songs.lib import retrieve_windows_encoding, strip_rtf
|
||||
from .songimport import SongImport
|
||||
|
||||
RTF_STRIPPING_REGEX = re.compile(r'\{\\tx[^}]*\}')
|
||||
# regex: at least two newlines, can have spaces between them
|
||||
SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*')
|
||||
NUMBER_REGEX = re.compile(r'[0-9]+')
|
||||
@ -77,9 +77,121 @@ class EasyWorshipSongImport(SongImport):
|
||||
|
||||
def do_import(self):
|
||||
"""
|
||||
Import the songs
|
||||
Determines the type of file to import and calls the appropiate method
|
||||
"""
|
||||
if self.import_source.lower().endswith('ews'):
|
||||
self.import_ews()
|
||||
else:
|
||||
self.import_db()
|
||||
|
||||
:return:
|
||||
def import_ews(self):
|
||||
"""
|
||||
Import the songs from service file
|
||||
The full spec of the ews files can be found here:
|
||||
https://github.com/meinders/lithium-ews/blob/master/docs/ews%20file%20format.md
|
||||
or here: http://wiki.openlp.org/Development:EasyWorship_EWS_Format
|
||||
"""
|
||||
# Open ews file if it exists
|
||||
if not os.path.isfile(self.import_source):
|
||||
log.debug('Given ews file does not exists.')
|
||||
return
|
||||
# Make sure there is room for at least a header and one entry
|
||||
if os.path.getsize(self.import_source) < 892:
|
||||
log.debug('Given ews file is to small to contain valid data.')
|
||||
return
|
||||
# Take a stab at how text is encoded
|
||||
self.encoding = 'cp1252'
|
||||
self.encoding = retrieve_windows_encoding(self.encoding)
|
||||
if not self.encoding:
|
||||
log.debug('No encoding set.')
|
||||
return
|
||||
self.ews_file = open(self.import_source, 'rb')
|
||||
# EWS header, version '1.6'/' 3'/' 5':
|
||||
# Offset Field Data type Length Details
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
# 0 Filetype string 38 Specifies the file type and version.
|
||||
# "EasyWorship Schedule File Version 1.6" or
|
||||
# "EasyWorship Schedule File Version 3" or
|
||||
# "EasyWorship Schedule File Version 5"
|
||||
# 40/48/56 Entry count int32le 4 Number of items in the schedule
|
||||
# 44/52/60 Entry length int16le 2 Length of schedule entries: 0x0718 = 1816
|
||||
# Get file version
|
||||
type, = struct.unpack('<38s', self.ews_file.read(38))
|
||||
version = type.decode()[-3:]
|
||||
# Set fileposition based on filetype/version
|
||||
file_pos = 0
|
||||
if version == ' 5':
|
||||
file_pos = 56
|
||||
elif version == ' 3':
|
||||
file_pos = 48
|
||||
elif version == '1.6':
|
||||
file_pos = 40
|
||||
else:
|
||||
log.debug('Given ews file is of unknown version.')
|
||||
return
|
||||
entry_count = self.get_i32(file_pos)
|
||||
entry_length = self.get_i16(file_pos+4)
|
||||
file_pos += 6
|
||||
self.import_wizard.progress_bar.setMaximum(entry_count)
|
||||
# Loop over songs
|
||||
for i in range(entry_count):
|
||||
# Load EWS entry metadata:
|
||||
# Offset Field Data type Length Details
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# 0 Title cstring 50
|
||||
# 307 Author cstring 50
|
||||
# 358 Copyright cstring 100
|
||||
# 459 Administrator cstring 50
|
||||
# 800 Content pointer int32le 4 Position of the content for this entry.
|
||||
# 820 Content type int32le 4 0x01 = Song, 0x02 = Scripture, 0x03 = Presentation,
|
||||
# 0x04 = Video, 0x05 = Live video, 0x07 = Image,
|
||||
# 0x08 = Audio, 0x09 = Web
|
||||
# 1410 Song number cstring 10
|
||||
self.set_defaults()
|
||||
self.title = self.get_string(file_pos + 0, 50)
|
||||
authors = self.get_string(file_pos + 307, 50)
|
||||
copyright = self.get_string(file_pos + 358, 100)
|
||||
admin = self.get_string(file_pos + 459, 50)
|
||||
cont_ptr = self.get_i32(file_pos + 800)
|
||||
cont_type = self.get_i32(file_pos + 820)
|
||||
self.ccli_number = self.get_string(file_pos + 1410, 10)
|
||||
# Only handle content type 1 (songs)
|
||||
if cont_type != 1:
|
||||
file_pos += entry_length
|
||||
continue
|
||||
# Load song content
|
||||
# Offset Field Data type Length Details
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# 0 Length int32le 4 Length (L) of content, including the compressed content
|
||||
# and the following fields (14 bytes total).
|
||||
# 4 Content string L-14 Content compressed with deflate.
|
||||
# Checksum int32be 4 Alder-32 checksum.
|
||||
# (unknown) 4 0x51 0x4b 0x03 0x04
|
||||
# Content length int32le 4 Length of content after decompression
|
||||
content_length = self.get_i32(cont_ptr)
|
||||
deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
|
||||
deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
|
||||
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
|
||||
if copyright:
|
||||
self.copyright = copyright
|
||||
if admin:
|
||||
if copyright:
|
||||
self.copyright += ', '
|
||||
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'Administered by %s') % admin
|
||||
# Set the SongImport object members.
|
||||
self.set_song_import_object(authors, inflated_content)
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
if not self.finish():
|
||||
self.log_error(self.import_source)
|
||||
# Set file_pos for next entry
|
||||
file_pos += entry_length
|
||||
self.ews_file.close()
|
||||
|
||||
def import_db(self):
|
||||
"""
|
||||
Import the songs from the database
|
||||
"""
|
||||
# Open the DB and MB files if they exist
|
||||
import_source_mb = self.import_source.replace('.DB', '.MB')
|
||||
@ -176,7 +288,6 @@ class EasyWorshipSongImport(SongImport):
|
||||
ccli = self.get_field(fi_ccli)
|
||||
authors = self.get_field(fi_author)
|
||||
words = self.get_field(fi_words)
|
||||
# Set the SongImport object members.
|
||||
if copy:
|
||||
self.copyright = copy.decode()
|
||||
if admin:
|
||||
@ -187,55 +298,11 @@ class EasyWorshipSongImport(SongImport):
|
||||
if ccli:
|
||||
self.ccli_number = ccli.decode()
|
||||
if authors:
|
||||
# Split up the authors
|
||||
author_list = authors.split(b'/')
|
||||
if len(author_list) < 2:
|
||||
author_list = authors.split(b';')
|
||||
if len(author_list) < 2:
|
||||
author_list = authors.split(b',')
|
||||
for author_name in author_list:
|
||||
self.add_author(author_name.decode().strip())
|
||||
if words:
|
||||
# Format the lyrics
|
||||
result = strip_rtf(words.decode(), self.encoding)
|
||||
if result is None:
|
||||
return
|
||||
words, self.encoding = result
|
||||
verse_type = VerseType.tags[VerseType.Verse]
|
||||
for verse in SLIDE_BREAK_REGEX.split(words):
|
||||
verse = verse.strip()
|
||||
if not verse:
|
||||
continue
|
||||
verse_split = verse.split('\n', 1)
|
||||
first_line_is_tag = False
|
||||
# EW tags: verse, chorus, pre-chorus, bridge, tag,
|
||||
# intro, ending, slide
|
||||
for tag in VerseType.tags + ['tag', 'slide']:
|
||||
tag = tag.lower()
|
||||
ew_tag = verse_split[0].strip().lower()
|
||||
if ew_tag.startswith(tag):
|
||||
verse_type = tag[0]
|
||||
if tag == 'tag' or tag == 'slide':
|
||||
verse_type = VerseType.tags[VerseType.Other]
|
||||
first_line_is_tag = True
|
||||
number_found = False
|
||||
# check if tag is followed by number and/or note
|
||||
if len(ew_tag) > len(tag):
|
||||
match = NUMBER_REGEX.search(ew_tag)
|
||||
if match:
|
||||
number = match.group()
|
||||
verse_type += number
|
||||
number_found = True
|
||||
match = NOTE_REGEX.search(ew_tag)
|
||||
if match:
|
||||
self.comments += ew_tag + '\n'
|
||||
if not number_found:
|
||||
verse_type += '1'
|
||||
break
|
||||
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
|
||||
if len(self.comments) > 5:
|
||||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
||||
authors = authors.decode()
|
||||
else:
|
||||
authors = ''
|
||||
# Set the SongImport object members.
|
||||
self.set_song_import_object(authors, words)
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
if not self.finish():
|
||||
@ -243,12 +310,69 @@ class EasyWorshipSongImport(SongImport):
|
||||
db_file.close()
|
||||
self.memo_file.close()
|
||||
|
||||
def set_song_import_object(self, authors, words):
|
||||
"""
|
||||
Set the SongImport object members.
|
||||
|
||||
:param authors: String with authons
|
||||
:param words: Bytes with rtf-encoding
|
||||
"""
|
||||
if authors:
|
||||
# Split up the authors
|
||||
author_list = authors.split('/')
|
||||
if len(author_list) < 2:
|
||||
author_list = authors.split(';')
|
||||
if len(author_list) < 2:
|
||||
author_list = authors.split(',')
|
||||
for author_name in author_list:
|
||||
self.add_author(author_name.strip())
|
||||
if words:
|
||||
# Format the lyrics
|
||||
result = strip_rtf(words.decode(), self.encoding)
|
||||
if result is None:
|
||||
return
|
||||
words, self.encoding = result
|
||||
verse_type = VerseType.tags[VerseType.Verse]
|
||||
for verse in SLIDE_BREAK_REGEX.split(words):
|
||||
verse = verse.strip()
|
||||
if not verse:
|
||||
continue
|
||||
verse_split = verse.split('\n', 1)
|
||||
first_line_is_tag = False
|
||||
# EW tags: verse, chorus, pre-chorus, bridge, tag,
|
||||
# intro, ending, slide
|
||||
for tag in VerseType.tags + ['tag', 'slide']:
|
||||
tag = tag.lower()
|
||||
ew_tag = verse_split[0].strip().lower()
|
||||
if ew_tag.startswith(tag):
|
||||
verse_type = tag[0]
|
||||
if tag == 'tag' or tag == 'slide':
|
||||
verse_type = VerseType.tags[VerseType.Other]
|
||||
first_line_is_tag = True
|
||||
number_found = False
|
||||
# check if tag is followed by number and/or note
|
||||
if len(ew_tag) > len(tag):
|
||||
match = NUMBER_REGEX.search(ew_tag)
|
||||
if match:
|
||||
number = match.group()
|
||||
verse_type += number
|
||||
number_found = True
|
||||
match = NOTE_REGEX.search(ew_tag)
|
||||
if match:
|
||||
self.comments += ew_tag + '\n'
|
||||
if not number_found:
|
||||
verse_type += '1'
|
||||
break
|
||||
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
|
||||
if len(self.comments) > 5:
|
||||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
||||
|
||||
def find_field(self, field_name):
|
||||
"""
|
||||
Find a field in the descriptions
|
||||
|
||||
:param field_name: field to find
|
||||
:return:
|
||||
"""
|
||||
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
|
||||
|
||||
@ -285,7 +409,7 @@ class EasyWorshipSongImport(SongImport):
|
||||
Extract the field
|
||||
|
||||
:param field_desc_index: Field index value
|
||||
:return:
|
||||
:return: The field value
|
||||
"""
|
||||
field = self.fields[field_desc_index]
|
||||
field_desc = self.field_descriptions[field_desc_index]
|
||||
@ -323,3 +447,52 @@ class EasyWorshipSongImport(SongImport):
|
||||
return self.memo_file.read(blob_size)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_bytes(self, pos, length):
|
||||
"""
|
||||
Get bytes from ews_file
|
||||
|
||||
:param pos: Position to read from
|
||||
:param length: Bytes to read
|
||||
:return: Bytes read
|
||||
"""
|
||||
self.ews_file.seek(pos)
|
||||
return self.ews_file.read(length)
|
||||
|
||||
def get_string(self, pos, length):
|
||||
"""
|
||||
Get string from ews_file
|
||||
|
||||
:param pos: Position to read from
|
||||
:param length: Characters to read
|
||||
:return: String read
|
||||
"""
|
||||
bytes = self.get_bytes(pos, length)
|
||||
mask = '<' + str(length) + 's'
|
||||
byte_str, = struct.unpack(mask, bytes)
|
||||
return byte_str.decode('unicode-escape').replace('\0', '').strip()
|
||||
|
||||
def get_i16(self, pos):
|
||||
"""
|
||||
Get short int from ews_file
|
||||
|
||||
:param pos: Position to read from
|
||||
:return: Short integer read
|
||||
"""
|
||||
|
||||
bytes = self.get_bytes(pos, 2)
|
||||
mask = '<h'
|
||||
number, = struct.unpack(mask, bytes)
|
||||
return number
|
||||
|
||||
def get_i32(self, pos):
|
||||
"""
|
||||
Get long int from ews_file
|
||||
|
||||
:param pos: Position to read from
|
||||
:return: Long integer read
|
||||
"""
|
||||
bytes = self.get_bytes(pos, 4)
|
||||
mask = '<i'
|
||||
number, = struct.unpack(mask, bytes)
|
||||
return number
|
||||
|
@ -153,19 +153,20 @@ class SongFormat(object):
|
||||
CCLI = 3
|
||||
DreamBeam = 4
|
||||
EasySlides = 5
|
||||
EasyWorship = 6
|
||||
FoilPresenter = 7
|
||||
MediaShout = 8
|
||||
OpenSong = 9
|
||||
PowerSong = 10
|
||||
SongBeamer = 11
|
||||
SongPro = 12
|
||||
SongShowPlus = 13
|
||||
SongsOfFellowship = 14
|
||||
SundayPlus = 15
|
||||
WordsOfWorship = 16
|
||||
WorshipCenterPro = 17
|
||||
ZionWorx = 18
|
||||
EasyWorshipDB = 6
|
||||
EasyWorshipService = 7
|
||||
FoilPresenter = 8
|
||||
MediaShout = 9
|
||||
OpenSong = 10
|
||||
PowerSong = 11
|
||||
SongBeamer = 12
|
||||
SongPro = 13
|
||||
SongShowPlus = 14
|
||||
SongsOfFellowship = 15
|
||||
SundayPlus = 16
|
||||
WordsOfWorship = 17
|
||||
WorshipCenterPro = 18
|
||||
ZionWorx = 19
|
||||
|
||||
# Set optional attribute defaults
|
||||
__defaults__ = {
|
||||
@ -224,13 +225,20 @@ class SongFormat(object):
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File')
|
||||
},
|
||||
EasyWorship: {
|
||||
EasyWorshipDB: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship',
|
||||
'name': 'EasyWorship Song Database',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '%s (*.db)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Song Database')
|
||||
},
|
||||
EasyWorshipService: {
|
||||
'class': EasyWorshipSongImport,
|
||||
'name': 'EasyWorship Service File',
|
||||
'prefix': 'ew',
|
||||
'selectMode': SongFormatSelect.SingleFile,
|
||||
'filter': '%s (*.ews)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Service File')
|
||||
},
|
||||
FoilPresenter: {
|
||||
'class': FoilPresenterImport,
|
||||
'name': 'Foilpresenter',
|
||||
@ -341,7 +349,8 @@ class SongFormat(object):
|
||||
SongFormat.CCLI,
|
||||
SongFormat.DreamBeam,
|
||||
SongFormat.EasySlides,
|
||||
SongFormat.EasyWorship,
|
||||
SongFormat.EasyWorshipDB,
|
||||
SongFormat.EasyWorshipService,
|
||||
SongFormat.FoilPresenter,
|
||||
SongFormat.MediaShout,
|
||||
SongFormat.OpenSong,
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
||||
``<authors>``
|
||||
OpenLP does not support the attribute *type* and *lang*.
|
||||
OpenLP does not support the attribute *lang*.
|
||||
|
||||
``<chord>``
|
||||
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):
|
||||
"""
|
||||
|
BIN
resources/images/network_auth.png
Normal file
BIN
resources/images/network_auth.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 608 B |
BIN
resources/images/network_server.png
Normal file
BIN
resources/images/network_server.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
resources/images/network_ssl.png
Normal file
BIN
resources/images/network_ssl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 577 B |
@ -149,6 +149,11 @@
|
||||
<file>messagebox_info.png</file>
|
||||
<file>messagebox_warning.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="remote">
|
||||
<file>network_server.png</file>
|
||||
<file>network_ssl.png</file>
|
||||
<file>network_auth.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="songusage">
|
||||
<file>song_usage_active.png</file>
|
||||
<file>song_usage_inactive.png</file>
|
||||
|
@ -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.
|
||||
|
@ -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 [
|
||||
|
@ -35,9 +35,107 @@ from PyQt4 import QtGui, QtCore
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.core.utils import ActionList
|
||||
from openlp.core.utils.actions import CategoryActionList
|
||||
from tests.functional import MagicMock
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestCategoryActionList(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Create an instance and a few example actions.
|
||||
"""
|
||||
self.action1 = MagicMock()
|
||||
self.action1.text.return_value = 'first'
|
||||
self.action2 = MagicMock()
|
||||
self.action2.text.return_value = 'second'
|
||||
self.list = CategoryActionList()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up
|
||||
"""
|
||||
del self.list
|
||||
|
||||
def contains_test(self):
|
||||
"""
|
||||
Test the __contains__() method
|
||||
"""
|
||||
# GIVEN: The list.
|
||||
# WHEN: Add an action
|
||||
self.list.append(self.action1)
|
||||
|
||||
# THEN: The actions should (not) be in the list.
|
||||
self.assertTrue(self.action1 in self.list)
|
||||
self.assertFalse(self.action2 in self.list)
|
||||
|
||||
def len_test(self):
|
||||
"""
|
||||
Test the __len__ method
|
||||
"""
|
||||
# GIVEN: The list.
|
||||
# WHEN: Do nothing.
|
||||
# THEN: Check the length.
|
||||
self.assertEqual(len(self.list), 0, "The length should be 0.")
|
||||
|
||||
# GIVEN: The list.
|
||||
# WHEN: Append an action.
|
||||
self.list.append(self.action1)
|
||||
|
||||
# THEN: Check the length.
|
||||
self.assertEqual(len(self.list), 1, "The length should be 1.")
|
||||
|
||||
def append_test(self):
|
||||
"""
|
||||
Test the append() method
|
||||
"""
|
||||
# GIVEN: The list.
|
||||
# WHEN: Append an action.
|
||||
self.list.append(self.action1)
|
||||
self.list.append(self.action2)
|
||||
|
||||
# THEN: Check if the actions are in the list and check if they have the correct weights.
|
||||
self.assertTrue(self.action1 in self.list)
|
||||
self.assertTrue(self.action2 in self.list)
|
||||
self.assertEqual(self.list.actions[0], (0, self.action1))
|
||||
self.assertEqual(self.list.actions[1], (1, self.action2))
|
||||
|
||||
def add_test(self):
|
||||
"""
|
||||
Test the add() method
|
||||
"""
|
||||
# GIVEN: The list and weights.
|
||||
action1_weight = 42
|
||||
action2_weight = 41
|
||||
|
||||
# WHEN: Add actions and their weights.
|
||||
self.list.add(self.action1, action1_weight)
|
||||
self.list.add(self.action2, action2_weight)
|
||||
|
||||
# THEN: Check if they were added and have the specified weights.
|
||||
self.assertTrue(self.action1 in self.list)
|
||||
self.assertTrue(self.action2 in self.list)
|
||||
# Now check if action1 is second and action2 is first (due to their weights).
|
||||
self.assertEqual(self.list.actions[0], (41, self.action2))
|
||||
self.assertEqual(self.list.actions[1], (42, self.action1))
|
||||
|
||||
def remove_test(self):
|
||||
"""
|
||||
Test the remove() method
|
||||
"""
|
||||
# GIVEN: The list
|
||||
self.list.append(self.action1)
|
||||
|
||||
# WHEN: Delete an item from the list.
|
||||
self.list.remove(self.action1)
|
||||
|
||||
# THEN: Now the element should not be in the list anymore.
|
||||
self.assertFalse(self.action1 in self.list)
|
||||
|
||||
# THEN: Check if an exception is raised when trying to remove a not present action.
|
||||
self.assertRaises(ValueError, self.list.remove, self.action2)
|
||||
|
||||
|
||||
class TestActionList(TestCase, TestMixin):
|
||||
"""
|
||||
Test the ActionList class
|
||||
|
@ -69,6 +69,20 @@ SONG_TEST_DATA = [
|
||||
'Just to bow and receive a new blessing,\nIn the beautiful garden of prayer.', 'v3')],
|
||||
'verse_order_list': []}]
|
||||
|
||||
EWS_SONG_TEST_DATA =\
|
||||
{'title': 'Vi pløjed og vi så\'de',
|
||||
'authors': ['Matthias Claudius'],
|
||||
'verses':
|
||||
[('Vi pløjed og vi så\'de\nvor sæd i sorten jord,\nså bad vi ham os hjælpe,\nsom højt i Himlen bor,\n'
|
||||
'og han lod snefald hegne\nmod frosten barsk og hård,\nhan lod det tø og regne\nog varme mildt i vår.',
|
||||
'v1'),
|
||||
('Alle gode gaver\nde kommer ovenned,\nså tak da Gud, ja, pris dog Gud\nfor al hans kærlighed!', 'c1'),
|
||||
('Han er jo den, hvis vilje\nopholder alle ting,\nhan klæder markens lilje\nog runder himlens ring,\n'
|
||||
'ham lyder vind og vove,\nham rører ravnes nød,\nhvi skulle ej hans småbørn\nda og få dagligt brød?', 'v2'),
|
||||
('Ja, tak, du kære Fader,\nså mild, så rig, så rund,\nfor korn i hæs og lader,\nfor godt i allen stund!\n'
|
||||
'Vi kan jo intet give,\nsom nogen ting er værd,\nmen tag vort stakkels hjerte,\nså ringe som det er!',
|
||||
'v3')]}
|
||||
|
||||
|
||||
class EasyWorshipSongImportLogger(EasyWorshipSongImport):
|
||||
"""
|
||||
@ -357,9 +371,9 @@ class TestEasyWorshipSongImport(TestCase):
|
||||
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
|
||||
mocked_retrieve_windows_encoding.assert_call(encoding)
|
||||
|
||||
def file_import_test(self):
|
||||
def db_file_import_test(self):
|
||||
"""
|
||||
Test the actual import of real song files and check that the imported data is correct.
|
||||
Test the actual import of real song database files and check that the imported data is correct.
|
||||
"""
|
||||
|
||||
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
|
||||
@ -386,10 +400,11 @@ class TestEasyWorshipSongImport(TestCase):
|
||||
|
||||
# WHEN: Importing each file
|
||||
importer.import_source = os.path.join(TEST_PATH, 'Songs.DB')
|
||||
import_result = importer.do_import()
|
||||
|
||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||
# called.
|
||||
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
|
||||
self.assertIsNone(import_result, 'do_import should return None when it has completed')
|
||||
for song_data in SONG_TEST_DATA:
|
||||
title = song_data['title']
|
||||
author_calls = song_data['authors']
|
||||
@ -411,3 +426,44 @@ class TestEasyWorshipSongImport(TestCase):
|
||||
self.assertEqual(importer.verse_order_list, verse_order_list,
|
||||
'verse_order_list for %s should be %s' % (title, verse_order_list))
|
||||
mocked_finish.assert_called_with()
|
||||
|
||||
def ews_file_import_test(self):
|
||||
"""
|
||||
Test the actual import of song from ews file and check that the imported data is correct.
|
||||
"""
|
||||
|
||||
# GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
|
||||
# and mocked out "author", "add_copyright", "add_verse", "finish" methods.
|
||||
with patch('openlp.plugins.songs.lib.ewimport.SongImport'), \
|
||||
patch('openlp.plugins.songs.lib.ewimport.retrieve_windows_encoding') \
|
||||
as mocked_retrieve_windows_encoding:
|
||||
mocked_retrieve_windows_encoding.return_value = 'cp1252'
|
||||
mocked_manager = MagicMock()
|
||||
mocked_import_wizard = MagicMock()
|
||||
mocked_add_author = MagicMock()
|
||||
mocked_add_verse = MagicMock()
|
||||
mocked_finish = MagicMock()
|
||||
mocked_title = MagicMock()
|
||||
mocked_finish.return_value = True
|
||||
importer = EasyWorshipSongImportLogger(mocked_manager)
|
||||
importer.import_wizard = mocked_import_wizard
|
||||
importer.stop_import_flag = False
|
||||
importer.add_author = mocked_add_author
|
||||
importer.add_verse = mocked_add_verse
|
||||
importer.title = mocked_title
|
||||
importer.finish = mocked_finish
|
||||
importer.topics = []
|
||||
|
||||
# WHEN: Importing ews file
|
||||
importer.import_source = os.path.join(TEST_PATH, 'test1.ews')
|
||||
import_result = importer.do_import()
|
||||
|
||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||
# called.
|
||||
title = EWS_SONG_TEST_DATA['title']
|
||||
self.assertIsNone(import_result, 'do_import should return None when it has completed')
|
||||
self.assertIn(title, importer._title_assignment_list, 'title for should be "%s"' % title)
|
||||
mocked_add_author.assert_any_call(EWS_SONG_TEST_DATA['authors'][0])
|
||||
for verse_text, verse_tag in EWS_SONG_TEST_DATA['verses']:
|
||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||
mocked_finish.assert_called_with()
|
||||
|
@ -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')
|
||||
|
78
tests/interfaces/openlp_core_ui/test_shortcutlistform.py
Normal file
78
tests/interfaces/openlp_core_ui/test_shortcutlistform.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2014 Raoul Snyman #
|
||||
# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan #
|
||||
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
|
||||
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
|
||||
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
|
||||
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
|
||||
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
|
||||
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Package to test the openlp.core.ui.shortcutform package.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from PyQt4 import QtCore, QtGui, QtTest
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.ui.shortcutlistform import ShortcutListForm
|
||||
from tests.interfaces import patch
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestShortcutform(TestCase, TestMixin):
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.get_application()
|
||||
self.main_window = QtGui.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.form = ShortcutListForm()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
del self.form
|
||||
del self.main_window
|
||||
|
||||
def adjust_button_test(self):
|
||||
"""
|
||||
Test the _adjust_button() method
|
||||
"""
|
||||
# GIVEN: A button.
|
||||
button = QtGui.QPushButton()
|
||||
checked = True
|
||||
enabled = True
|
||||
text = "new!"
|
||||
|
||||
# WHEN: Call the method.
|
||||
with patch('PyQt4.QtGui.QPushButton.setChecked') as mocked_check_method:
|
||||
self.form._adjust_button(button, checked, enabled, text)
|
||||
|
||||
# THEN: The button should be changed.
|
||||
self.assertEqual(button.text(), text, "The text should match.")
|
||||
mocked_check_method.assert_called_once_with(True)
|
||||
self.assertEqual(button.isEnabled(), enabled, "The button should be disabled.")
|
BIN
tests/resources/easyworshipsongs/test1.ews
Normal file
BIN
tests/resources/easyworshipsongs/test1.ews
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user