forked from openlp/openlp
head
This commit is contained in:
commit
2ba7fa4f44
|
@ -2,9 +2,12 @@
|
||||||
*.*~
|
*.*~
|
||||||
\#*\#
|
\#*\#
|
||||||
*.eric4project
|
*.eric4project
|
||||||
|
*.eric5project
|
||||||
*.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
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
The :mod:`openlp` module contains all the project produced OpenLP functionality
|
The :mod:`openlp` module contains all the project produced OpenLP functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import openlp.core
|
from openlp import core, plugins
|
||||||
import openlp.plugins
|
|
||||||
|
|
||||||
__all__ = ['core', 'plugins']
|
__all__ = ['core', 'plugins']
|
||||||
|
|
|
@ -76,6 +76,9 @@ def check_directory_exists(directory, do_not_log=False):
|
||||||
def get_frozen_path(frozen_option, non_frozen_option):
|
def get_frozen_path(frozen_option, non_frozen_option):
|
||||||
"""
|
"""
|
||||||
Return a path based on the system status.
|
Return a path based on the system status.
|
||||||
|
|
||||||
|
:param frozen_option:
|
||||||
|
:param non_frozen_option:
|
||||||
"""
|
"""
|
||||||
if hasattr(sys, 'frozen') and sys.frozen == 1:
|
if hasattr(sys, 'frozen') and sys.frozen == 1:
|
||||||
return frozen_option
|
return frozen_option
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -168,29 +168,29 @@ class MediaManagerItem(QtGui.QWidget, RegistryProperties):
|
||||||
Create buttons for the media item toolbar
|
Create buttons for the media item toolbar
|
||||||
"""
|
"""
|
||||||
toolbar_actions = []
|
toolbar_actions = []
|
||||||
## Import Button ##
|
# Import Button
|
||||||
if self.has_import_icon:
|
if self.has_import_icon:
|
||||||
toolbar_actions.append(['Import', StringContent.Import,
|
toolbar_actions.append(['Import', StringContent.Import,
|
||||||
':/general/general_import.png', self.on_import_click])
|
':/general/general_import.png', self.on_import_click])
|
||||||
## Load Button ##
|
# Load Button
|
||||||
if self.has_file_icon:
|
if self.has_file_icon:
|
||||||
toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click])
|
toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click])
|
||||||
## New Button ##
|
# New Button
|
||||||
if self.has_new_icon:
|
if self.has_new_icon:
|
||||||
toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click])
|
toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click])
|
||||||
## Edit Button ##
|
# Edit Button
|
||||||
if self.has_edit_icon:
|
if self.has_edit_icon:
|
||||||
toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click])
|
toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click])
|
||||||
## Delete Button ##
|
# Delete Button
|
||||||
if self.has_delete_icon:
|
if self.has_delete_icon:
|
||||||
toolbar_actions.append(['Delete', StringContent.Delete,
|
toolbar_actions.append(['Delete', StringContent.Delete,
|
||||||
':/general/general_delete.png', self.on_delete_click])
|
':/general/general_delete.png', self.on_delete_click])
|
||||||
## Preview ##
|
# Preview
|
||||||
toolbar_actions.append(['Preview', StringContent.Preview,
|
toolbar_actions.append(['Preview', StringContent.Preview,
|
||||||
':/general/general_preview.png', self.on_preview_click])
|
':/general/general_preview.png', self.on_preview_click])
|
||||||
## Live Button ##
|
# Live Button
|
||||||
toolbar_actions.append(['Live', StringContent.Live, ':/general/general_live.png', self.on_live_click])
|
toolbar_actions.append(['Live', StringContent.Live, ':/general/general_live.png', self.on_live_click])
|
||||||
## Add to service Button ##
|
# Add to service Button
|
||||||
toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click])
|
toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click])
|
||||||
for action in toolbar_actions:
|
for action in toolbar_actions:
|
||||||
if action[0] == StringContent.Preview:
|
if action[0] == StringContent.Preview:
|
||||||
|
|
|
@ -101,7 +101,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
|
||||||
``add_import_menu_item(import_menu)``
|
``add_import_menu_item(import_menu)``
|
||||||
Add an item to the Import menu.
|
Add an item to the Import menu.
|
||||||
|
|
||||||
``add_export_menu_Item(export_menu)``
|
``add_export_menu_item(export_menu)``
|
||||||
Add an item to the Export menu.
|
Add an item to the Export menu.
|
||||||
|
|
||||||
``create_settings_tab()``
|
``create_settings_tab()``
|
||||||
|
@ -226,7 +226,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_export_menu_Item(self, export_menu):
|
def add_export_menu_item(self, export_menu):
|
||||||
"""
|
"""
|
||||||
Create a menu item and add it to the "Export" menu.
|
Create a menu item and add it to the "Export" menu.
|
||||||
|
|
||||||
|
@ -329,22 +329,24 @@ class Plugin(QtCore.QObject, RegistryProperties):
|
||||||
def set_plugin_ui_text_strings(self, tooltips):
|
def set_plugin_ui_text_strings(self, tooltips):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
|
|
||||||
|
:param tooltips:
|
||||||
"""
|
"""
|
||||||
## Load Action ##
|
# Load Action
|
||||||
self.__set_name_text_string(StringContent.Load, UiStrings().Load, tooltips['load'])
|
self.__set_name_text_string(StringContent.Load, UiStrings().Load, tooltips['load'])
|
||||||
## Import Action ##
|
# Import Action
|
||||||
self.__set_name_text_string(StringContent.Import, UiStrings().Import, tooltips['import'])
|
self.__set_name_text_string(StringContent.Import, UiStrings().Import, tooltips['import'])
|
||||||
## New Action ##
|
# New Action
|
||||||
self.__set_name_text_string(StringContent.New, UiStrings().Add, tooltips['new'])
|
self.__set_name_text_string(StringContent.New, UiStrings().Add, tooltips['new'])
|
||||||
## Edit Action ##
|
# Edit Action
|
||||||
self.__set_name_text_string(StringContent.Edit, UiStrings().Edit, tooltips['edit'])
|
self.__set_name_text_string(StringContent.Edit, UiStrings().Edit, tooltips['edit'])
|
||||||
## Delete Action ##
|
# Delete Action
|
||||||
self.__set_name_text_string(StringContent.Delete, UiStrings().Delete, tooltips['delete'])
|
self.__set_name_text_string(StringContent.Delete, UiStrings().Delete, tooltips['delete'])
|
||||||
## Preview Action ##
|
# Preview Action
|
||||||
self.__set_name_text_string(StringContent.Preview, UiStrings().Preview, tooltips['preview'])
|
self.__set_name_text_string(StringContent.Preview, UiStrings().Preview, tooltips['preview'])
|
||||||
## Send Live Action ##
|
# Send Live Action
|
||||||
self.__set_name_text_string(StringContent.Live, UiStrings().Live, tooltips['live'])
|
self.__set_name_text_string(StringContent.Live, UiStrings().Live, tooltips['live'])
|
||||||
## Add to Service Action ##
|
# Add to Service Action
|
||||||
self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service'])
|
self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service'])
|
||||||
|
|
||||||
def __set_name_text_string(self, name, title, tooltip):
|
def __set_name_text_string(self, name, title, tooltip):
|
||||||
|
|
|
@ -161,7 +161,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
for plugin in self.plugins:
|
for plugin in self.plugins:
|
||||||
if plugin.status is not PluginStatus.Disabled:
|
if plugin.status is not PluginStatus.Disabled:
|
||||||
plugin.add_export_menu_Item(self.main_window.file_export_menu)
|
plugin.add_export_menu_item(self.main_window.file_export_menu)
|
||||||
|
|
||||||
def hook_tools_menu(self):
|
def hook_tools_menu(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -248,6 +248,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||||
elif item.is_capable(ItemCapabilities.CanSoftBreak):
|
elif item.is_capable(ItemCapabilities.CanSoftBreak):
|
||||||
pages = []
|
pages = []
|
||||||
if '[---]' in text:
|
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:
|
while True:
|
||||||
slides = text.split('\n[---]\n', 2)
|
slides = text.split('\n[---]\n', 2)
|
||||||
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
|
# 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.
|
off when displayed.
|
||||||
|
|
||||||
:param lines: The text to be fitted on the slide split into lines.
|
: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 = []
|
formatted = []
|
||||||
previous_html = ''
|
previous_html = ''
|
||||||
|
@ -416,7 +419,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||||
processed word by word. This is sometimes need for **bible** verses.
|
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 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 = []
|
formatted = []
|
||||||
previous_html = ''
|
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::
|
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
|
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.
|
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.
|
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.
|
: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.
|
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 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 ``u' '`` or ``u'<br>``. This is needed for
|
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
|
||||||
bibles.
|
bibles.
|
||||||
"""
|
"""
|
||||||
smallest_index = 0
|
smallest_index = 0
|
||||||
|
|
|
@ -387,7 +387,7 @@ class ServiceItem(RegistryProperties):
|
||||||
self.will_auto_start = header.get('will_auto_start', False)
|
self.will_auto_start = header.get('will_auto_start', False)
|
||||||
self.processor = header.get('processor', None)
|
self.processor = header.get('processor', None)
|
||||||
self.has_original_files = True
|
self.has_original_files = True
|
||||||
#TODO Remove me in 2,3 build phase
|
# TODO: Remove me in 2,3 build phase
|
||||||
if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay):
|
if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay):
|
||||||
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
|
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
|
||||||
self.processor = self.title
|
self.processor = self.title
|
||||||
|
|
|
@ -173,7 +173,7 @@ def create_button(parent, name, **kwargs):
|
||||||
kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
|
kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
|
||||||
else:
|
else:
|
||||||
log.warn('The role "%s" is not defined in create_push_button().', role)
|
log.warn('The role "%s" is not defined in create_push_button().', role)
|
||||||
if kwargs.pop('class', '') == 'toolbutton':
|
if kwargs.pop('btn_class', '') == 'toolbutton':
|
||||||
button = QtGui.QToolButton(parent)
|
button = QtGui.QToolButton(parent)
|
||||||
else:
|
else:
|
||||||
button = QtGui.QPushButton(parent)
|
button = QtGui.QPushButton(parent)
|
||||||
|
|
|
@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab):
|
||||||
"""
|
"""
|
||||||
Select an image for the default display screen.
|
Select an image for the default display screen.
|
||||||
"""
|
"""
|
||||||
file_filters = '%s;;%s (*.*) (*)' % (get_images_filter(), UiStrings().AllFiles)
|
file_filters = '%s;;%s (*.*)' % (get_images_filter(), UiStrings().AllFiles)
|
||||||
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.AdvancedTab', 'Open File'), '',
|
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.AdvancedTab', 'Open File'), '',
|
||||||
file_filters)
|
file_filters)
|
||||||
if filename:
|
if filename:
|
||||||
|
|
|
@ -95,12 +95,12 @@ class Ui_ExceptionDialog(object):
|
||||||
Translate the widgets on the fly.
|
Translate the widgets on the fly.
|
||||||
"""
|
"""
|
||||||
exception_dialog.setWindowTitle(translate('OpenLP.ExceptionDialog', 'Error Occurred'))
|
exception_dialog.setWindowTitle(translate('OpenLP.ExceptionDialog', 'Error Occurred'))
|
||||||
self.description_explanation.setText(translate('OpenLP.ExceptionDialog',
|
self.description_explanation.setText(
|
||||||
'Please enter a description of what you were doing to cause this error '
|
translate('OpenLP.ExceptionDialog', 'Please enter a description of what you were doing to cause this error '
|
||||||
'\n(Minimum 20 characters)'))
|
'\n(Minimum 20 characters)'))
|
||||||
self.message_label.setText(translate('OpenLP.ExceptionDialog', 'Oops! '
|
self.message_label.setText(
|
||||||
'OpenLP hit a problem, and couldn\'t recover. The text in the box '
|
translate('OpenLP.ExceptionDialog', 'Oops! OpenLP hit a problem, and couldn\'t recover. The text in the '
|
||||||
'below contains information that might be helpful to the OpenLP '
|
'box below contains information that might be helpful to the OpenLP '
|
||||||
'developers, so please e-mail it to bugs@openlp.org, along with a '
|
'developers, so please e-mail it to bugs@openlp.org, along with a '
|
||||||
'detailed description of what you were doing when the problem '
|
'detailed description of what you were doing when the problem '
|
||||||
'occurred.'))
|
'occurred.'))
|
||||||
|
|
|
@ -228,7 +228,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
|
files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
|
||||||
Settings().value(self.settings_section + '/last directory'),
|
Settings().value(self.settings_section + '/last directory'),
|
||||||
'%s (*.*) (*)' % UiStrings().AllFiles)
|
'%s (*)' % UiStrings().AllFiles)
|
||||||
log.info('New files(s) %s', str(files))
|
log.info('New files(s) %s', str(files))
|
||||||
if files:
|
if files:
|
||||||
self.file_attachment = str(files)
|
self.file_attachment = str(files)
|
||||||
|
|
|
@ -67,7 +67,7 @@ class ThemeScreenshotThread(QtCore.QThread):
|
||||||
title = config.get('theme_%s' % theme, 'title')
|
title = config.get('theme_%s' % theme, 'title')
|
||||||
filename = config.get('theme_%s' % theme, 'filename')
|
filename = config.get('theme_%s' % theme, 'filename')
|
||||||
screenshot = config.get('theme_%s' % theme, 'screenshot')
|
screenshot = config.get('theme_%s' % theme, 'screenshot')
|
||||||
urllib.request.urlretrieve('%s%s' % (self.parent().web, screenshot),
|
urllib.request.urlretrieve('%s%s' % (self.parent().themes_url, screenshot),
|
||||||
os.path.join(gettempdir(), 'openlp', screenshot))
|
os.path.join(gettempdir(), 'openlp', screenshot))
|
||||||
item = QtGui.QListWidgetItem(title, self.parent().themes_list_widget)
|
item = QtGui.QListWidgetItem(title, self.parent().themes_list_widget)
|
||||||
item.setData(QtCore.Qt.UserRole, filename)
|
item.setData(QtCore.Qt.UserRole, filename)
|
||||||
|
@ -96,6 +96,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
if self.web_access:
|
if self.web_access:
|
||||||
files = self.web_access.read()
|
files = self.web_access.read()
|
||||||
self.config.read_string(files.decode())
|
self.config.read_string(files.decode())
|
||||||
|
self.web = self.config.get('general', 'base url')
|
||||||
|
self.songs_url = self.web + self.config.get('songs', 'directory') + '/'
|
||||||
|
self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/'
|
||||||
|
self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
|
||||||
self.update_screen_list_combo()
|
self.update_screen_list_combo()
|
||||||
self.was_download_cancelled = False
|
self.was_download_cancelled = False
|
||||||
self.theme_screenshot_thread = None
|
self.theme_screenshot_thread = None
|
||||||
|
@ -110,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.
|
||||||
"""
|
"""
|
||||||
|
@ -341,7 +345,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
item = self.songs_list_widget.item(i)
|
item = self.songs_list_widget.item(i)
|
||||||
if item.checkState() == QtCore.Qt.Checked:
|
if item.checkState() == QtCore.Qt.Checked:
|
||||||
filename = item.data(QtCore.Qt.UserRole)
|
filename = item.data(QtCore.Qt.UserRole)
|
||||||
size = self._get_file_size('%s%s' % (self.web, filename))
|
size = self._get_file_size('%s%s' % (self.songs_url, filename))
|
||||||
self.max_progress += size
|
self.max_progress += size
|
||||||
# Loop through the Bibles list and increase for each selected item
|
# Loop through the Bibles list and increase for each selected item
|
||||||
iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget)
|
iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget)
|
||||||
|
@ -350,7 +354,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
item = iterator.value()
|
item = iterator.value()
|
||||||
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
|
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
|
||||||
filename = item.data(0, QtCore.Qt.UserRole)
|
filename = item.data(0, QtCore.Qt.UserRole)
|
||||||
size = self._get_file_size('%s%s' % (self.web, filename))
|
size = self._get_file_size('%s%s' % (self.bibles_url, filename))
|
||||||
self.max_progress += size
|
self.max_progress += size
|
||||||
iterator += 1
|
iterator += 1
|
||||||
# Loop through the themes list and increase for each selected item
|
# Loop through the themes list and increase for each selected item
|
||||||
|
@ -359,7 +363,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
item = self.themes_list_widget.item(i)
|
item = self.themes_list_widget.item(i)
|
||||||
if item.checkState() == QtCore.Qt.Checked:
|
if item.checkState() == QtCore.Qt.Checked:
|
||||||
filename = item.data(QtCore.Qt.UserRole)
|
filename = item.data(QtCore.Qt.UserRole)
|
||||||
size = self._get_file_size('%s%s' % (self.web, filename))
|
size = self._get_file_size('%s%s' % (self.themes_url, filename))
|
||||||
self.max_progress += size
|
self.max_progress += size
|
||||||
if self.max_progress:
|
if self.max_progress:
|
||||||
# Add on 2 for plugins status setting plus a "finished" point.
|
# Add on 2 for plugins status setting plus a "finished" point.
|
||||||
|
@ -435,7 +439,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
self._increment_progress_bar(self.downloading % filename, 0)
|
self._increment_progress_bar(self.downloading % filename, 0)
|
||||||
self.previous_size = 0
|
self.previous_size = 0
|
||||||
destination = os.path.join(songs_destination, str(filename))
|
destination = os.path.join(songs_destination, str(filename))
|
||||||
self.url_get_file('%s%s' % (self.web, filename), destination)
|
self.url_get_file('%s%s' % (self.songs_url, filename), destination)
|
||||||
# Download Bibles
|
# Download Bibles
|
||||||
bibles_iterator = QtGui.QTreeWidgetItemIterator(
|
bibles_iterator = QtGui.QTreeWidgetItemIterator(
|
||||||
self.bibles_tree_widget)
|
self.bibles_tree_widget)
|
||||||
|
@ -445,7 +449,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
bible = item.data(0, QtCore.Qt.UserRole)
|
bible = item.data(0, QtCore.Qt.UserRole)
|
||||||
self._increment_progress_bar(self.downloading % bible, 0)
|
self._increment_progress_bar(self.downloading % bible, 0)
|
||||||
self.previous_size = 0
|
self.previous_size = 0
|
||||||
self.url_get_file('%s%s' % (self.web, bible), os.path.join(bibles_destination, bible))
|
self.url_get_file('%s%s' % (self.bibles_url, bible), os.path.join(bibles_destination, bible))
|
||||||
bibles_iterator += 1
|
bibles_iterator += 1
|
||||||
# Download themes
|
# Download themes
|
||||||
for i in range(self.themes_list_widget.count()):
|
for i in range(self.themes_list_widget.count()):
|
||||||
|
@ -454,7 +458,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
|
||||||
theme = item.data(QtCore.Qt.UserRole)
|
theme = item.data(QtCore.Qt.UserRole)
|
||||||
self._increment_progress_bar(self.downloading % theme, 0)
|
self._increment_progress_bar(self.downloading % theme, 0)
|
||||||
self.previous_size = 0
|
self.previous_size = 0
|
||||||
self.url_get_file('%s%s' % (self.web, theme), os.path.join(themes_destination, theme))
|
self.url_get_file('%s%s' % (self.themes_url, theme), os.path.join(themes_destination, theme))
|
||||||
# Set Default Display
|
# Set Default Display
|
||||||
if self.display_combo_box.currentIndex() != -1:
|
if self.display_combo_box.currentIndex() != -1:
|
||||||
Settings().setValue('core/monitor', self.display_combo_box.currentIndex())
|
Settings().setValue('core/monitor', self.display_combo_box.currentIndex())
|
||||||
|
|
|
@ -211,8 +211,8 @@ class Ui_FirstTimeWizard(object):
|
||||||
first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard'))
|
first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard'))
|
||||||
self.title_label.setText('<span style="font-size:14pt; font-weight:600;">%s</span>' %
|
self.title_label.setText('<span style="font-size:14pt; font-weight:600;">%s</span>' %
|
||||||
translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard'))
|
translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard'))
|
||||||
self.information_label.setText(translate('OpenLP.FirstTimeWizard',
|
self.information_label.setText(
|
||||||
'This wizard will help you to configure OpenLP for initial use. '
|
translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. '
|
||||||
'Click the next button below to start.'))
|
'Click the next button below to start.'))
|
||||||
self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins'))
|
self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins'))
|
||||||
self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. '))
|
self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. '))
|
||||||
|
|
|
@ -63,7 +63,6 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont
|
||||||
self.services = FormattingTagController()
|
self.services = FormattingTagController()
|
||||||
self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected)
|
self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected)
|
||||||
self.new_button.clicked.connect(self.on_new_clicked)
|
self.new_button.clicked.connect(self.on_new_clicked)
|
||||||
#self.save_button.clicked.connect(self.on_saved_clicked)
|
|
||||||
self.delete_button.clicked.connect(self.on_delete_clicked)
|
self.delete_button.clicked.connect(self.on_delete_clicked)
|
||||||
self.tag_table_widget.currentCellChanged.connect(self.on_current_cell_changed)
|
self.tag_table_widget.currentCellChanged.connect(self.on_current_cell_changed)
|
||||||
self.button_box.rejected.connect(self.close)
|
self.button_box.rejected.connect(self.close)
|
||||||
|
@ -202,5 +201,4 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont
|
||||||
if errors:
|
if errors:
|
||||||
QtGui.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors,
|
QtGui.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors,
|
||||||
QtGui.QMessageBox.Ok)
|
QtGui.QMessageBox.Ok)
|
||||||
#self.tag_table_widget.selectRow(pre_row - 1)
|
|
||||||
self.tag_table_widget.resizeRowsToContents()
|
self.tag_table_widget.resizeRowsToContents()
|
||||||
|
|
|
@ -168,8 +168,10 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
if enabled:
|
if enabled:
|
||||||
self.setAutoFillBackground(False)
|
self.setAutoFillBackground(False)
|
||||||
|
self.setStyleSheet("QGraphicsView {background: transparent; border: 0px;}")
|
||||||
else:
|
else:
|
||||||
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False)
|
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False)
|
||||||
|
self.setStyleSheet("QGraphicsView {}")
|
||||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled)
|
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled)
|
||||||
self.repaint()
|
self.repaint()
|
||||||
|
|
||||||
|
@ -350,7 +352,6 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||||
self.hide_display(self.hide_mode)
|
self.hide_display(self.hide_mode)
|
||||||
# Only continue if the visibility wasn't changed during method call.
|
# Only continue if the visibility wasn't changed during method call.
|
||||||
elif was_visible == self.isVisible():
|
elif was_visible == self.isVisible():
|
||||||
|
|
||||||
# Single screen active
|
# Single screen active
|
||||||
if self.screens.display_count == 1:
|
if self.screens.display_count == 1:
|
||||||
# Only make visible if setting enabled.
|
# Only make visible if setting enabled.
|
||||||
|
|
|
@ -61,16 +61,14 @@ MEDIA_MANAGER_STYLE = """
|
||||||
}
|
}
|
||||||
QToolBox::tab {
|
QToolBox::tab {
|
||||||
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||||
stop: 0 palette(button), stop: 0.5 palette(button),
|
stop: 0 palette(button), stop: 1.0 palette(mid));
|
||||||
stop: 1.0 palette(mid));
|
border: 1px solid palette(mid);
|
||||||
border: 1px groove palette(mid);
|
border-radius: 3px;
|
||||||
border-radius: 5px;
|
|
||||||
}
|
}
|
||||||
QToolBox::tab:selected {
|
QToolBox::tab:selected {
|
||||||
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||||
stop: 0 palette(light), stop: 0.5 palette(midlight),
|
stop: 0 palette(light), stop: 1.0 palette(button));
|
||||||
stop: 1.0 palette(dark));
|
border: 1px solid palette(mid);
|
||||||
border: 1px groove palette(dark);
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
@ -369,7 +367,7 @@ class Ui_MainWindow(object):
|
||||||
self.settings_menu.setTitle(translate('OpenLP.MainWindow', '&Settings'))
|
self.settings_menu.setTitle(translate('OpenLP.MainWindow', '&Settings'))
|
||||||
self.settings_language_menu.setTitle(translate('OpenLP.MainWindow', '&Language'))
|
self.settings_language_menu.setTitle(translate('OpenLP.MainWindow', '&Language'))
|
||||||
self.help_menu.setTitle(translate('OpenLP.MainWindow', '&Help'))
|
self.help_menu.setTitle(translate('OpenLP.MainWindow', '&Help'))
|
||||||
self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Media Manager'))
|
self.media_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Library'))
|
||||||
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager'))
|
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager'))
|
||||||
self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
|
self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
|
||||||
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
|
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
|
||||||
|
@ -396,11 +394,11 @@ class Ui_MainWindow(object):
|
||||||
self.settings_shortcuts_item.setText(translate('OpenLP.MainWindow', 'Configure &Shortcuts...'))
|
self.settings_shortcuts_item.setText(translate('OpenLP.MainWindow', 'Configure &Shortcuts...'))
|
||||||
self.formatting_tag_item.setText(translate('OpenLP.MainWindow', 'Configure &Formatting Tags...'))
|
self.formatting_tag_item.setText(translate('OpenLP.MainWindow', 'Configure &Formatting Tags...'))
|
||||||
self.settings_configure_item.setText(translate('OpenLP.MainWindow', '&Configure OpenLP...'))
|
self.settings_configure_item.setText(translate('OpenLP.MainWindow', '&Configure OpenLP...'))
|
||||||
self.settings_export_item.setStatusTip(translate('OpenLP.MainWindow',
|
self.settings_export_item.setStatusTip(
|
||||||
'Export OpenLP settings to a specified *.config file'))
|
translate('OpenLP.MainWindow', 'Export OpenLP settings to a specified *.config file'))
|
||||||
self.settings_export_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
self.settings_export_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
||||||
self.settings_import_item.setStatusTip(translate('OpenLP.MainWindow',
|
self.settings_import_item.setStatusTip(
|
||||||
'Import OpenLP settings from a specified *.config file previously '
|
translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously '
|
||||||
'exported on this or another machine'))
|
'exported on this or another machine'))
|
||||||
self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
self.settings_import_item.setText(translate('OpenLP.MainWindow', 'Settings'))
|
||||||
self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager'))
|
self.view_media_manager_item.setText(translate('OpenLP.MainWindow', '&Media Manager'))
|
||||||
|
@ -862,7 +860,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||||
section = 'general'
|
section = 'general'
|
||||||
section_key = section + "/" + key
|
section_key = section + "/" + key
|
||||||
# Make sure it's a valid section for us.
|
# Make sure it's a valid section for us.
|
||||||
if not section in setting_sections:
|
if section not in setting_sections:
|
||||||
continue
|
continue
|
||||||
# We have a good file, import it.
|
# We have a good file, import it.
|
||||||
for section_key in import_keys:
|
for section_key in import_keys:
|
||||||
|
@ -1336,7 +1334,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||||
if self.copy_data:
|
if self.copy_data:
|
||||||
log.info('Copying data to new path')
|
log.info('Copying data to new path')
|
||||||
try:
|
try:
|
||||||
self.showStatusMessage(
|
self.show_status_message(
|
||||||
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
|
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
|
||||||
'- Please wait for copy to finish').replace('%s', self.new_data_path))
|
'- Please wait for copy to finish').replace('%s', self.new_data_path))
|
||||||
dir_util.copy_tree(old_data_path, self.new_data_path)
|
dir_util.copy_tree(old_data_path, self.new_data_path)
|
||||||
|
@ -1366,8 +1364,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||||
args = []
|
args = []
|
||||||
for a in self.arguments:
|
for a in self.arguments:
|
||||||
args.extend([a])
|
args.extend([a])
|
||||||
for arg in args:
|
for filename in args:
|
||||||
filename = arg
|
|
||||||
if not isinstance(filename, str):
|
if not isinstance(filename, str):
|
||||||
filename = str(filename, sys.getfilesystemencoding())
|
filename = str(filename, sys.getfilesystemencoding())
|
||||||
if filename.endswith(('.osz', '.oszl')):
|
if filename.endswith(('.osz', '.oszl')):
|
||||||
|
|
|
@ -90,7 +90,7 @@ def get_media_players():
|
||||||
overridden_player = 'auto'
|
overridden_player = 'auto'
|
||||||
else:
|
else:
|
||||||
overridden_player = ''
|
overridden_player = ''
|
||||||
saved_players_list = saved_players.replace('[', '').replace(']', '').split(',')
|
saved_players_list = saved_players.replace('[', '').replace(']', '').split(',') if saved_players else []
|
||||||
return saved_players_list, overridden_player
|
return saved_players_list, overridden_player
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -137,7 +137,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
for player in list(self.media_players.values()):
|
for player in list(self.media_players.values()):
|
||||||
if player.is_active:
|
if player.is_active:
|
||||||
for item in player.audio_extensions_list:
|
for item in player.audio_extensions_list:
|
||||||
if not item in self.audio_extensions_list:
|
if item not in self.audio_extensions_list:
|
||||||
self.audio_extensions_list.append(item)
|
self.audio_extensions_list.append(item)
|
||||||
suffix_list.append(item[2:])
|
suffix_list.append(item[2:])
|
||||||
self.video_extensions_list = []
|
self.video_extensions_list = []
|
||||||
|
@ -184,8 +184,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
return False
|
return False
|
||||||
saved_players, overridden_player = get_media_players()
|
saved_players, overridden_player = get_media_players()
|
||||||
invalid_media_players = \
|
invalid_media_players = \
|
||||||
[mediaPlayer for mediaPlayer in saved_players if not mediaPlayer in self.media_players or
|
[media_player for media_player in saved_players if media_player not in self.media_players or
|
||||||
not self.media_players[mediaPlayer].check_available()]
|
not self.media_players[media_player].check_available()]
|
||||||
if invalid_media_players:
|
if invalid_media_players:
|
||||||
for invalidPlayer in invalid_media_players:
|
for invalidPlayer in invalid_media_players:
|
||||||
saved_players.remove(invalidPlayer)
|
saved_players.remove(invalidPlayer)
|
||||||
|
@ -506,6 +506,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
else:
|
else:
|
||||||
self.media_volume(controller, controller.media_info.volume)
|
self.media_volume(controller, controller.media_info.volume)
|
||||||
if status:
|
if status:
|
||||||
|
if not controller.media_info.is_background:
|
||||||
display.frame.evaluateJavaScript('show_blank("desktop");')
|
display.frame.evaluateJavaScript('show_blank("desktop");')
|
||||||
self.current_media_players[controller.controller_type].set_visible(display, True)
|
self.current_media_players[controller.controller_type].set_visible(display, True)
|
||||||
# Flash needs to be played and will not AutoPlay
|
# Flash needs to be played and will not AutoPlay
|
||||||
|
@ -517,7 +518,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||||
controller.mediabar.actions['playbackPause'].setVisible(True)
|
controller.mediabar.actions['playbackPause'].setVisible(True)
|
||||||
controller.mediabar.actions['playbackStop'].setVisible(True)
|
controller.mediabar.actions['playbackStop'].setVisible(True)
|
||||||
if controller.is_live:
|
if controller.is_live:
|
||||||
if controller.hide_menu.defaultAction().isChecked():
|
if controller.hide_menu.defaultAction().isChecked() and not controller.media_info.is_background:
|
||||||
controller.hide_menu.defaultAction().trigger()
|
controller.hide_menu.defaultAction().trigger()
|
||||||
# Start Timer for ui updates
|
# Start Timer for ui updates
|
||||||
if not self.timer.isActive():
|
if not self.timer.isActive():
|
||||||
|
|
|
@ -174,34 +174,11 @@ FLASH_HTML = """
|
||||||
<div id="flash" class="size" style="visibility:hidden"></div>
|
<div id="flash" class="size" style="visibility:hidden"></div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VIDEO_EXT = [
|
VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov',
|
||||||
'*.3gp',
|
'*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg',
|
||||||
'*.3gpp',
|
'*.avi', '*.swf']
|
||||||
'*.3g2',
|
|
||||||
'*.3gpp2',
|
|
||||||
'*.aac',
|
|
||||||
'*.flv',
|
|
||||||
'*.f4a',
|
|
||||||
'*.f4b',
|
|
||||||
'*.f4p',
|
|
||||||
'*.f4v',
|
|
||||||
'*.mov',
|
|
||||||
'*.m4a',
|
|
||||||
'*.m4b',
|
|
||||||
'*.m4p',
|
|
||||||
'*.m4v',
|
|
||||||
'*.mkv',
|
|
||||||
'*.mp4',
|
|
||||||
'*.ogv',
|
|
||||||
'*.webm',
|
|
||||||
'*.mpg', '*.wmv', '*.mpeg', '*.avi',
|
|
||||||
'*.swf'
|
|
||||||
]
|
|
||||||
|
|
||||||
AUDIO_EXT = [
|
AUDIO_EXT = ['*.mp3', '*.ogg']
|
||||||
'*.mp3',
|
|
||||||
'*.ogg'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WebkitPlayer(MediaPlayer):
|
class WebkitPlayer(MediaPlayer):
|
||||||
|
@ -411,10 +388,9 @@ class WebkitPlayer(MediaPlayer):
|
||||||
"""
|
"""
|
||||||
Return some information about this player
|
Return some information about this player
|
||||||
"""
|
"""
|
||||||
return(translate('Media.player', 'Webkit is a media player which runs '
|
part1 = translate('Media.player', 'Webkit is a media player which runs inside a web browser. This player '
|
||||||
'inside a web browser. This player allows text over video to be '
|
'allows text over video to be rendered.')
|
||||||
'rendered.') +
|
part2 = translate('Media.player', 'Audio')
|
||||||
'<br/> <strong>' + translate('Media.player', 'Audio') +
|
part3 = translate('Media.player', 'Video')
|
||||||
'</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' +
|
return part1 + '<br/> <strong>' + part2 + '</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' + part3 + \
|
||||||
translate('Media.player', 'Video') + '</strong><br/>' +
|
'</strong><br/>' + str(VIDEO_EXT) + '<br/>'
|
||||||
str(VIDEO_EXT) + '<br/>')
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -235,7 +235,8 @@ class Ui_ServiceManager(object):
|
||||||
self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Edit Item'),
|
self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Edit Item'),
|
||||||
icon=':/general/general_edit.png', triggers=self.remote_edit)
|
icon=':/general/general_edit.png', triggers=self.remote_edit)
|
||||||
self.rename_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Rename...'),
|
self.rename_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Rename...'),
|
||||||
icon=':/general/general_edit.png', triggers=self.on_service_item_rename)
|
icon=':/general/general_edit.png',
|
||||||
|
triggers=self.on_service_item_rename)
|
||||||
self.maintain_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Reorder Item'),
|
self.maintain_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Reorder Item'),
|
||||||
icon=':/general/general_edit.png',
|
icon=':/general/general_edit.png',
|
||||||
triggers=self.on_service_item_edit_form)
|
triggers=self.on_service_item_edit_form)
|
||||||
|
@ -401,7 +402,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
|
||||||
:param suffix_list: New Suffix's to be supported
|
:param suffix_list: New Suffix's to be supported
|
||||||
"""
|
"""
|
||||||
for suffix in suffix_list:
|
for suffix in suffix_list:
|
||||||
if not suffix in self.suffixes:
|
if suffix not in self.suffixes:
|
||||||
self.suffixes.append(suffix)
|
self.suffixes.append(suffix)
|
||||||
|
|
||||||
def on_new_service_clicked(self, field=None):
|
def on_new_service_clicked(self, field=None):
|
||||||
|
@ -756,8 +757,9 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
|
||||||
items = json.load(file_to)
|
items = json.load(file_to)
|
||||||
else:
|
else:
|
||||||
critical_error_message_box(message=translate('OpenLP.ServiceManager',
|
critical_error_message_box(message=translate('OpenLP.ServiceManager',
|
||||||
'The service file you are trying to open is in an old format.\n '
|
'The service file you are trying to open is in an old '
|
||||||
'Please save it using OpenLP 2.0.2 or greater.'))
|
'format.\n Please save it using OpenLP 2.0.2 or '
|
||||||
|
'greater.'))
|
||||||
return
|
return
|
||||||
file_to.close()
|
file_to.close()
|
||||||
self.new_file()
|
self.new_file()
|
||||||
|
@ -1487,9 +1489,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
|
||||||
if new_item:
|
if new_item:
|
||||||
self.add_service_item(new_item, replace=True)
|
self.add_service_item(new_item, replace=True)
|
||||||
|
|
||||||
def on_service_item_rename(self):
|
def on_service_item_rename(self, field=None):
|
||||||
"""
|
"""
|
||||||
Opens a dialog to rename the service item.
|
Opens a dialog to rename the service item.
|
||||||
|
|
||||||
|
:param field: Not used, but PyQt needs this.
|
||||||
"""
|
"""
|
||||||
item = self.find_service_item()[0]
|
item = self.find_service_item()[0]
|
||||||
if not self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEditTitle):
|
if not self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEditTitle):
|
||||||
|
|
|
@ -150,5 +150,5 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties):
|
||||||
|
|
||||||
:param function: The function to be called
|
:param function: The function to be called
|
||||||
"""
|
"""
|
||||||
if not function in self.processes:
|
if function not in self.processes:
|
||||||
self.processes.append(function)
|
self.processes.append(function)
|
||||||
|
|
|
@ -244,10 +244,10 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||||
self.primary_push_button.setChecked(False)
|
self.primary_push_button.setChecked(False)
|
||||||
self.alternate_push_button.setChecked(False)
|
self.alternate_push_button.setChecked(False)
|
||||||
else:
|
else:
|
||||||
if action.defaultShortcuts:
|
if action.default_shortcuts:
|
||||||
primary_label_text = action.defaultShortcuts[0].toString()
|
primary_label_text = action.default_shortcuts[0].toString()
|
||||||
if len(action.defaultShortcuts) == 2:
|
if len(action.default_shortcuts) == 2:
|
||||||
alternate_label_text = action.defaultShortcuts[1].toString()
|
alternate_label_text = action.default_shortcuts[1].toString()
|
||||||
shortcuts = self._action_shortcuts(action)
|
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
|
# 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.
|
# 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='')
|
self._adjust_button(self.alternate_push_button, False, text='')
|
||||||
for category in self.action_list.categories:
|
for category in self.action_list.categories:
|
||||||
for action in category.actions:
|
for action in category.actions:
|
||||||
self.changed_actions[action] = action.defaultShortcuts
|
self.changed_actions[action] = action.default_shortcuts
|
||||||
self.refresh_shortcut_list()
|
self.refresh_shortcut_list()
|
||||||
|
|
||||||
def on_default_radio_button_clicked(self, toggled):
|
def on_default_radio_button_clicked(self, toggled):
|
||||||
|
@ -306,7 +306,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||||
if action is None:
|
if action is None:
|
||||||
return
|
return
|
||||||
temp_shortcuts = self._action_shortcuts(action)
|
temp_shortcuts = self._action_shortcuts(action)
|
||||||
self.changed_actions[action] = action.defaultShortcuts
|
self.changed_actions[action] = action.default_shortcuts
|
||||||
self.refresh_shortcut_list()
|
self.refresh_shortcut_list()
|
||||||
primary_button_text = ''
|
primary_button_text = ''
|
||||||
alternate_button_text = ''
|
alternate_button_text = ''
|
||||||
|
@ -357,8 +357,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
|
||||||
return
|
return
|
||||||
shortcuts = self._action_shortcuts(action)
|
shortcuts = self._action_shortcuts(action)
|
||||||
new_shortcuts = []
|
new_shortcuts = []
|
||||||
if action.defaultShortcuts:
|
if action.default_shortcuts:
|
||||||
new_shortcuts.append(action.defaultShortcuts[0])
|
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
|
# 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
|
# 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
|
# 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 = []
|
new_shortcuts = []
|
||||||
if shortcuts:
|
if shortcuts:
|
||||||
new_shortcuts.append(shortcuts[0])
|
new_shortcuts.append(shortcuts[0])
|
||||||
if len(action.defaultShortcuts) == 2:
|
if len(action.default_shortcuts) == 2:
|
||||||
new_shortcuts.append(action.defaultShortcuts[1])
|
new_shortcuts.append(action.default_shortcuts[1])
|
||||||
if len(new_shortcuts) == 2:
|
if len(new_shortcuts) == 2:
|
||||||
if not self._validiate_shortcut(action, new_shortcuts[1]):
|
if not self._validiate_shortcut(action, new_shortcuts[1]):
|
||||||
return
|
return
|
||||||
|
|
|
@ -495,14 +495,14 @@ class SlideController(DisplayController, RegistryProperties):
|
||||||
self.on_theme_display(False)
|
self.on_theme_display(False)
|
||||||
self.on_hide_display(False)
|
self.on_hide_display(False)
|
||||||
|
|
||||||
def service_previous(self):
|
def service_previous(self, field=None):
|
||||||
"""
|
"""
|
||||||
Live event to select the previous service item from the service manager.
|
Live event to select the previous service item from the service manager.
|
||||||
"""
|
"""
|
||||||
self.keypress_queue.append(ServiceItemAction.Previous)
|
self.keypress_queue.append(ServiceItemAction.Previous)
|
||||||
self._process_queue()
|
self._process_queue()
|
||||||
|
|
||||||
def service_next(self):
|
def service_next(self, field=None):
|
||||||
"""
|
"""
|
||||||
Live event to select the next service item from the service manager.
|
Live event to select the next service item from the service manager.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -432,7 +432,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||||
Background Image button pushed.
|
Background Image button pushed.
|
||||||
"""
|
"""
|
||||||
images_filter = get_images_filter()
|
images_filter = get_images_filter()
|
||||||
images_filter = '%s;;%s (*.*) (*)' % (images_filter, UiStrings().AllFiles)
|
images_filter = '%s;;%s (*.*)' % (images_filter, UiStrings().AllFiles)
|
||||||
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.ThemeWizard', 'Select Image'), '',
|
filename = QtGui.QFileDialog.getOpenFileName(self, translate('OpenLP.ThemeWizard', 'Select Image'), '',
|
||||||
images_filter)
|
images_filter)
|
||||||
if filename:
|
if filename:
|
||||||
|
|
|
@ -44,7 +44,6 @@ class Ui_ThemeLayoutDialog(object):
|
||||||
Set up the UI
|
Set up the UI
|
||||||
"""
|
"""
|
||||||
themeLayoutDialog.setObjectName('themeLayoutDialogDialog')
|
themeLayoutDialog.setObjectName('themeLayoutDialogDialog')
|
||||||
#themeLayoutDialog.resize(300, 200)
|
|
||||||
self.preview_layout = QtGui.QVBoxLayout(themeLayoutDialog)
|
self.preview_layout = QtGui.QVBoxLayout(themeLayoutDialog)
|
||||||
self.preview_layout.setObjectName('preview_layout')
|
self.preview_layout.setObjectName('preview_layout')
|
||||||
self.preview_area = QtGui.QWidget(themeLayoutDialog)
|
self.preview_area = QtGui.QWidget(themeLayoutDialog)
|
||||||
|
|
|
@ -114,17 +114,19 @@ class ThemesTab(SettingsTab):
|
||||||
self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme'))
|
self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme'))
|
||||||
self.level_group_box.setTitle(translate('OpenLP.ThemesTab', 'Theme Level'))
|
self.level_group_box.setTitle(translate('OpenLP.ThemesTab', 'Theme Level'))
|
||||||
self.song_level_radio_button.setText(translate('OpenLP.ThemesTab', 'S&ong Level'))
|
self.song_level_radio_button.setText(translate('OpenLP.ThemesTab', 'S&ong Level'))
|
||||||
self.song_level_label.setText(translate('OpenLP.ThemesTab', 'Use the theme from each song '
|
self.song_level_label.setText(
|
||||||
'in the database. If a song doesn\'t have a theme associated with '
|
translate('OpenLP.ThemesTab', 'Use the theme from each song in the database. If a song doesn\'t have a '
|
||||||
'it, then use the service\'s theme. If the service doesn\'t have '
|
'theme associated with it, then use the service\'s theme. If the service '
|
||||||
'a theme, then use the global theme.'))
|
'doesn\'t have a theme, then use the global theme.'))
|
||||||
self.service_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Service Level'))
|
self.service_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Service Level'))
|
||||||
self.service_level_label.setText(translate('OpenLP.ThemesTab', 'Use the theme from the service, '
|
self.service_level_label.setText(
|
||||||
'overriding any of the individual songs\' themes. If the '
|
translate('OpenLP.ThemesTab', 'Use the theme from the service, overriding any of the individual '
|
||||||
'service doesn\'t have a theme, then use the global theme.'))
|
'songs\' themes. If the service doesn\'t have a theme, then use the global '
|
||||||
|
'theme.'))
|
||||||
self.global_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Global Level'))
|
self.global_level_radio_button.setText(translate('OpenLP.ThemesTab', '&Global Level'))
|
||||||
self.global_level_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding '
|
self.global_level_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding any themes '
|
||||||
'any themes associated with either the service or the songs.'))
|
'associated with either the service or the '
|
||||||
|
'songs.'))
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -65,20 +65,14 @@ class CategoryActionList(object):
|
||||||
self.index = 0
|
self.index = 0
|
||||||
self.actions = []
|
self.actions = []
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __contains__(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):
|
|
||||||
"""
|
"""
|
||||||
Implement the __contains__() method to make this class a dictionary type
|
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):
|
def __len__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -103,23 +97,14 @@ class CategoryActionList(object):
|
||||||
self.index += 1
|
self.index += 1
|
||||||
return self.actions[self.index - 1][1]
|
return self.actions[self.index - 1][1]
|
||||||
|
|
||||||
def has_key(self, key):
|
def append(self, action):
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Append an action
|
Append an action
|
||||||
"""
|
"""
|
||||||
weight = 0
|
weight = 0
|
||||||
if self.actions:
|
if self.actions:
|
||||||
weight = self.actions[-1][0] + 1
|
weight = self.actions[-1][0] + 1
|
||||||
self.add(name, weight)
|
self.add(action, weight)
|
||||||
|
|
||||||
def add(self, action, weight=0):
|
def add(self, action, weight=0):
|
||||||
"""
|
"""
|
||||||
|
@ -128,14 +113,15 @@ class CategoryActionList(object):
|
||||||
self.actions.append((weight, action))
|
self.actions.append((weight, action))
|
||||||
self.actions.sort(key=lambda act: act[0])
|
self.actions.sort(key=lambda act: act[0])
|
||||||
|
|
||||||
def remove(self, remove_action):
|
def remove(self, action):
|
||||||
"""
|
"""
|
||||||
Remove an action
|
Remove an action
|
||||||
"""
|
"""
|
||||||
for action in self.actions:
|
for item in self.actions:
|
||||||
if action[1] == remove_action:
|
if item[1] == action:
|
||||||
self.actions.remove(action)
|
self.actions.remove(item)
|
||||||
return
|
return
|
||||||
|
raise ValueError('Action "%s" does not exist.' % action)
|
||||||
|
|
||||||
|
|
||||||
class CategoryList(object):
|
class CategoryList(object):
|
||||||
|
@ -184,9 +170,9 @@ class CategoryList(object):
|
||||||
self.index += 1
|
self.index += 1
|
||||||
return self.categories[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:
|
for category in self.categories:
|
||||||
if category.name == key:
|
if category.name == key:
|
||||||
|
@ -200,10 +186,7 @@ class CategoryList(object):
|
||||||
weight = 0
|
weight = 0
|
||||||
if self.categories:
|
if self.categories:
|
||||||
weight = self.categories[-1].weight + 1
|
weight = self.categories[-1].weight + 1
|
||||||
if actions:
|
|
||||||
self.add(name, weight, actions)
|
self.add(name, weight, actions)
|
||||||
else:
|
|
||||||
self.add(name, weight)
|
|
||||||
|
|
||||||
def add(self, name, weight=0, actions=None):
|
def add(self, name, weight=0, actions=None):
|
||||||
"""
|
"""
|
||||||
|
@ -226,6 +209,8 @@ class CategoryList(object):
|
||||||
for category in self.categories:
|
for category in self.categories:
|
||||||
if category.name == name:
|
if category.name == name:
|
||||||
self.categories.remove(category)
|
self.categories.remove(category)
|
||||||
|
return
|
||||||
|
raise ValueError('Category "%s" does not exist.' % name)
|
||||||
|
|
||||||
|
|
||||||
class ActionList(object):
|
class ActionList(object):
|
||||||
|
@ -270,7 +255,7 @@ class ActionList(object):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
settings.beginGroup('shortcuts')
|
settings.beginGroup('shortcuts')
|
||||||
# Get the default shortcut from the config.
|
# 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:
|
if weight is None:
|
||||||
self.categories[category].actions.append(action)
|
self.categories[category].actions.append(action)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -207,12 +207,12 @@ class AlertsPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('AlertsPlugin', 'Alert', 'name singular'),
|
'singular': translate('AlertsPlugin', 'Alert', 'name singular'),
|
||||||
'plural': translate('AlertsPlugin', 'Alerts', 'name plural')
|
'plural': translate('AlertsPlugin', 'Alerts', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('AlertsPlugin', 'Alerts', 'container title')
|
'title': translate('AlertsPlugin', 'Alerts', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,8 +88,6 @@ class BiblePlugin(Plugin):
|
||||||
self.import_bible_item.setVisible(True)
|
self.import_bible_item.setVisible(True)
|
||||||
action_list = ActionList.get_instance()
|
action_list = ActionList.get_instance()
|
||||||
action_list.add_action(self.import_bible_item, UiStrings().Import)
|
action_list.add_action(self.import_bible_item, UiStrings().Import)
|
||||||
# Do not add the action to the list yet.
|
|
||||||
#action_list.add_action(self.export_bible_item, UiStrings().Export)
|
|
||||||
# Set to invisible until we can export bibles
|
# Set to invisible until we can export bibles
|
||||||
self.export_bible_item.setVisible(False)
|
self.export_bible_item.setVisible(False)
|
||||||
self.tools_upgrade_item.setVisible(bool(self.manager.old_bible_databases))
|
self.tools_upgrade_item.setVisible(bool(self.manager.old_bible_databases))
|
||||||
|
@ -104,7 +102,6 @@ class BiblePlugin(Plugin):
|
||||||
action_list = ActionList.get_instance()
|
action_list = ActionList.get_instance()
|
||||||
action_list.remove_action(self.import_bible_item, UiStrings().Import)
|
action_list.remove_action(self.import_bible_item, UiStrings().Import)
|
||||||
self.import_bible_item.setVisible(False)
|
self.import_bible_item.setVisible(False)
|
||||||
#action_list.remove_action(self.export_bible_item, UiStrings().Export)
|
|
||||||
self.export_bible_item.setVisible(False)
|
self.export_bible_item.setVisible(False)
|
||||||
|
|
||||||
def app_startup(self):
|
def app_startup(self):
|
||||||
|
@ -115,19 +112,27 @@ class BiblePlugin(Plugin):
|
||||||
if self.manager.old_bible_databases:
|
if self.manager.old_bible_databases:
|
||||||
if QtGui.QMessageBox.information(
|
if QtGui.QMessageBox.information(
|
||||||
self.main_window, translate('OpenLP', 'Information'),
|
self.main_window, translate('OpenLP', 'Information'),
|
||||||
translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your existing Bibles.\n'
|
translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your '
|
||||||
'Should OpenLP upgrade now?'),
|
'existing Bibles.\nShould OpenLP upgrade now?'),
|
||||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \
|
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \
|
||||||
QtGui.QMessageBox.Yes:
|
QtGui.QMessageBox.Yes:
|
||||||
self.on_tools_upgrade_Item_triggered()
|
self.on_tools_upgrade_Item_triggered()
|
||||||
|
|
||||||
def add_import_menu_item(self, import_menu):
|
def add_import_menu_item(self, import_menu):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param import_menu:
|
||||||
|
"""
|
||||||
self.import_bible_item = create_action(import_menu, 'importBibleItem',
|
self.import_bible_item = create_action(import_menu, 'importBibleItem',
|
||||||
text=translate('BiblesPlugin', '&Bible'), visible=False,
|
text=translate('BiblesPlugin', '&Bible'), visible=False,
|
||||||
triggers=self.on_bible_import_click)
|
triggers=self.on_bible_import_click)
|
||||||
import_menu.addAction(self.import_bible_item)
|
import_menu.addAction(self.import_bible_item)
|
||||||
|
|
||||||
def add_export_menu_Item(self, export_menu):
|
def add_export_menu_item(self, export_menu):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param export_menu:
|
||||||
|
"""
|
||||||
self.export_bible_item = create_action(export_menu, 'exportBibleItem',
|
self.export_bible_item = create_action(export_menu, 'exportBibleItem',
|
||||||
text=translate('BiblesPlugin', '&Bible'), visible=False)
|
text=translate('BiblesPlugin', '&Bible'), visible=False)
|
||||||
export_menu.addAction(self.export_bible_item)
|
export_menu.addAction(self.export_bible_item)
|
||||||
|
@ -190,12 +195,12 @@ class BiblePlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('BiblesPlugin', 'Bible', 'name singular'),
|
'singular': translate('BiblesPlugin', 'Bible', 'name singular'),
|
||||||
'plural': translate('BiblesPlugin', 'Bibles', 'name plural')
|
'plural': translate('BiblesPlugin', 'Bibles', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('BiblesPlugin', 'Bibles', 'container title')
|
'title': translate('BiblesPlugin', 'Bibles', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -32,9 +32,11 @@ import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
from PyQt4 import QtCore
|
from PyQt4 import QtCore
|
||||||
from sqlalchemy import Column, ForeignKey, Table, or_, types, func
|
from sqlalchemy import Column, ForeignKey, Table, or_, types, func
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.orm import class_mapper, mapper, relation
|
from sqlalchemy.orm import class_mapper, mapper, relation
|
||||||
from sqlalchemy.orm.exc import UnmappedClassError
|
from sqlalchemy.orm.exc import UnmappedClassError
|
||||||
|
|
||||||
|
@ -235,6 +237,11 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
|
||||||
text=verse_text
|
text=verse_text
|
||||||
)
|
)
|
||||||
self.session.add(verse)
|
self.session.add(verse)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
except OperationalError:
|
||||||
|
# Wait 10ms and try again (lp#1154467)
|
||||||
|
time.sleep(0.01)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
def create_verse(self, book_id, chapter, verse, text):
|
def create_verse(self, book_id, chapter, verse, text):
|
||||||
|
|
|
@ -552,10 +552,10 @@ class HTTPBible(BibleDB, RegistryProperties):
|
||||||
self.application.set_busy_cursor()
|
self.application.set_busy_cursor()
|
||||||
search_results = self.get_chapter(book, reference[1])
|
search_results = self.get_chapter(book, reference[1])
|
||||||
if search_results and search_results.has_verse_list():
|
if search_results and search_results.has_verse_list():
|
||||||
## We have found a book of the bible lets check to see
|
# We have found a book of the bible lets check to see
|
||||||
## if it was there. By reusing the returned book name
|
# if it was there. By reusing the returned book name
|
||||||
## we get a correct book. For example it is possible
|
# we get a correct book. For example it is possible
|
||||||
## to request ac and get Acts back.
|
# to request ac and get Acts back.
|
||||||
book_name = search_results.book
|
book_name = search_results.book
|
||||||
self.application.process_events()
|
self.application.process_events()
|
||||||
# Check to see if book/chapter exists.
|
# Check to see if book/chapter exists.
|
||||||
|
|
|
@ -480,6 +480,10 @@ class BibleMediaItem(MediaManagerItem):
|
||||||
self.reload_bibles()
|
self.reload_bibles()
|
||||||
|
|
||||||
def on_delete_click(self):
|
def on_delete_click(self):
|
||||||
|
"""
|
||||||
|
When the delete button is pressed
|
||||||
|
"""
|
||||||
|
bible = None
|
||||||
if self.quickTab.isVisible():
|
if self.quickTab.isVisible():
|
||||||
bible = self.quickVersionComboBox.currentText()
|
bible = self.quickVersionComboBox.currentText()
|
||||||
elif self.advancedTab.isVisible():
|
elif self.advancedTab.isVisible():
|
||||||
|
@ -488,7 +492,8 @@ class BibleMediaItem(MediaManagerItem):
|
||||||
if QtGui.QMessageBox.question(
|
if QtGui.QMessageBox.question(
|
||||||
self, UiStrings().ConfirmDelete,
|
self, UiStrings().ConfirmDelete,
|
||||||
translate('BiblesPlugin.MediaItem', 'Are you sure you want to completely delete "%s" Bible from '
|
translate('BiblesPlugin.MediaItem', 'Are you sure you want to completely delete "%s" Bible from '
|
||||||
'OpenLP?\n\nYou will need to re-import this Bible to use it again.') % bible,
|
'OpenLP?\n\nYou will need to re-import this Bible to use it '
|
||||||
|
'again.') % bible,
|
||||||
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No),
|
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No),
|
||||||
QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No:
|
QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No:
|
||||||
return
|
return
|
||||||
|
|
|
@ -73,13 +73,13 @@ class OpenSongBible(BibleDB):
|
||||||
log.debug('Starting OpenSong import from "%s"' % self.filename)
|
log.debug('Starting OpenSong import from "%s"' % self.filename)
|
||||||
if not isinstance(self.filename, str):
|
if not isinstance(self.filename, str):
|
||||||
self.filename = str(self.filename, 'utf8')
|
self.filename = str(self.filename, 'utf8')
|
||||||
file = None
|
import_file = None
|
||||||
success = True
|
success = True
|
||||||
try:
|
try:
|
||||||
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
# NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding
|
||||||
# detection, and the two mechanisms together interfere with each other.
|
# detection, and the two mechanisms together interfere with each other.
|
||||||
file = open(self.filename, 'r')
|
import_file = open(self.filename, 'rb')
|
||||||
opensong = objectify.parse(file)
|
opensong = objectify.parse(import_file)
|
||||||
bible = opensong.getroot()
|
bible = opensong.getroot()
|
||||||
language_id = self.get_language(bible_name)
|
language_id = self.get_language(bible_name)
|
||||||
if not language_id:
|
if not language_id:
|
||||||
|
@ -93,7 +93,7 @@ class OpenSongBible(BibleDB):
|
||||||
log.error('Importing books from "%s" failed' % self.filename)
|
log.error('Importing books from "%s" failed' % self.filename)
|
||||||
return False
|
return False
|
||||||
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
book_details = BiblesResourcesDB.get_book_by_id(book_ref_id)
|
||||||
db_book = self.create_book(str(book.attrib['n']), book_ref_id, book_details['testament_id'])
|
db_book = self.create_book(book.attrib['n'], book_ref_id, book_details['testament_id'])
|
||||||
chapter_number = 0
|
chapter_number = 0
|
||||||
for chapter in book.c:
|
for chapter in book.c:
|
||||||
if self.stop_import_flag:
|
if self.stop_import_flag:
|
||||||
|
@ -122,8 +122,8 @@ class OpenSongBible(BibleDB):
|
||||||
verse_number += 1
|
verse_number += 1
|
||||||
self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse))
|
self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse))
|
||||||
self.wizard.increment_progress_bar(
|
self.wizard.increment_progress_bar(
|
||||||
translate('BiblesPlugin.Opensong', 'Importing %s %s...',
|
translate('BiblesPlugin.Opensong', 'Importing %(bookname)s %(chapter)s...' %
|
||||||
'Importing <book name> <chapter>...')) % (db_book.name, chapter_number)
|
{'bookname': db_book.name, 'chapter': chapter_number}))
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.application.process_events()
|
self.application.process_events()
|
||||||
except etree.XMLSyntaxError as inst:
|
except etree.XMLSyntaxError as inst:
|
||||||
|
@ -137,8 +137,8 @@ class OpenSongBible(BibleDB):
|
||||||
log.exception('Loading Bible from OpenSong file failed')
|
log.exception('Loading Bible from OpenSong file failed')
|
||||||
success = False
|
success = False
|
||||||
finally:
|
finally:
|
||||||
if file:
|
if import_file:
|
||||||
file.close()
|
import_file.close()
|
||||||
if self.stop_import_flag:
|
if self.stop_import_flag:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -97,12 +97,12 @@ class CustomPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('CustomPlugin', 'Custom Slide', 'name singular'),
|
'singular': translate('CustomPlugin', 'Custom Slide', 'name singular'),
|
||||||
'plural': translate('CustomPlugin', 'Custom Slides', 'name plural')
|
'plural': translate('CustomPlugin', 'Custom Slides', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('CustomPlugin', 'Custom Slides', 'container title')
|
'title': translate('CustomPlugin', 'Custom Slides', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#lint:disable
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
|
|
@ -95,12 +95,12 @@ class ImagePlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin.
|
Called to define all translatable texts of the plugin.
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('ImagePlugin', 'Image', 'name singular'),
|
'singular': translate('ImagePlugin', 'Image', 'name singular'),
|
||||||
'plural': translate('ImagePlugin', 'Images', 'name plural')
|
'plural': translate('ImagePlugin', 'Images', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {'title': translate('ImagePlugin', 'Images', 'container title')}
|
self.text_strings[StringContent.VisibleName] = {'title': translate('ImagePlugin', 'Images', 'container title')}
|
||||||
# Middle Header Bar
|
# Middle Header Bar
|
||||||
tooltips = {
|
tooltips = {
|
||||||
|
|
|
@ -75,7 +75,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
|
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
|
||||||
file_formats = get_images_filter()
|
file_formats = get_images_filter()
|
||||||
self.on_new_file_masks = '%s;;%s (*.*) (*)' % (file_formats, UiStrings().AllFiles)
|
self.on_new_file_masks = '%s;;%s (*)' % (file_formats, UiStrings().AllFiles)
|
||||||
self.add_group_action.setText(UiStrings().AddGroup)
|
self.add_group_action.setText(UiStrings().AddGroup)
|
||||||
self.add_group_action.setToolTip(UiStrings().AddGroup)
|
self.add_group_action.setToolTip(UiStrings().AddGroup)
|
||||||
self.replace_action.setText(UiStrings().ReplaceBG)
|
self.replace_action.setText(UiStrings().ReplaceBG)
|
||||||
|
|
|
@ -95,7 +95,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||||
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
|
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
|
||||||
self.automatic = UiStrings().Automatic
|
self.automatic = UiStrings().Automatic
|
||||||
self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
|
self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
|
||||||
#self.rebuild_players()
|
|
||||||
|
|
||||||
def required_icons(self):
|
def required_icons(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -73,12 +73,12 @@ class MediaPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('MediaPlugin', 'Media', 'name singular'),
|
'singular': translate('MediaPlugin', 'Media', 'name singular'),
|
||||||
'plural': translate('MediaPlugin', 'Media', 'name plural')
|
'plural': translate('MediaPlugin', 'Media', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('MediaPlugin', 'Media', 'container title')
|
'title': translate('MediaPlugin', 'Media', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,8 +377,6 @@ class ImpressDocument(PresentationDocument):
|
||||||
Stop the presentation, remove from screen.
|
Stop the presentation, remove from screen.
|
||||||
"""
|
"""
|
||||||
log.debug('stop presentation OpenOffice')
|
log.debug('stop presentation OpenOffice')
|
||||||
# deactivate should hide the screen according to docs, but doesn't
|
|
||||||
#self.control.deactivate()
|
|
||||||
self.presentation.end()
|
self.presentation.end()
|
||||||
self.control = None
|
self.control = None
|
||||||
|
|
||||||
|
|
|
@ -204,19 +204,19 @@ class PdfDocument(PresentationDocument):
|
||||||
log.debug(' '.join(e.cmd))
|
log.debug(' '.join(e.cmd))
|
||||||
log.debug(e.output)
|
log.debug(e.output)
|
||||||
# Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
|
# Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
|
||||||
width = 0
|
width = 0.0
|
||||||
height = 0
|
height = 0.0
|
||||||
for line in runlog.splitlines():
|
for line in runlog.splitlines():
|
||||||
try:
|
try:
|
||||||
width = int(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
|
width = float(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
|
||||||
height = int(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
|
height = float(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
|
||||||
break
|
break
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
continue
|
||||||
# Calculate the ratio from pdf to screen
|
# Calculate the ratio from pdf to screen
|
||||||
if width > 0 and height > 0:
|
if width > 0 and height > 0:
|
||||||
width_ratio = size.right() / float(width)
|
width_ratio = size.right() / width
|
||||||
height_ratio = size.bottom() / float(height)
|
height_ratio = size.bottom() / height
|
||||||
# return the resolution that should be used. 72 is default.
|
# return the resolution that should be used. 72 is default.
|
||||||
if width_ratio > height_ratio:
|
if width_ratio > height_ratio:
|
||||||
return int(height_ratio * 72)
|
return int(height_ratio * 72)
|
||||||
|
|
|
@ -122,7 +122,9 @@ class PresentationDocument(object):
|
||||||
a file, e.g. thumbnails
|
a file, e.g. thumbnails
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if os.path.exists(self.get_thumbnail_folder()):
|
||||||
shutil.rmtree(self.get_thumbnail_folder())
|
shutil.rmtree(self.get_thumbnail_folder())
|
||||||
|
if os.path.exists(self.get_temp_folder()):
|
||||||
shutil.rmtree(self.get_temp_folder())
|
shutil.rmtree(self.get_temp_folder())
|
||||||
except OSError:
|
except OSError:
|
||||||
log.exception('Failed to delete presentation controller files')
|
log.exception('Failed to delete presentation controller files')
|
||||||
|
|
|
@ -156,12 +156,12 @@ class PresentationPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin.
|
Called to define all translatable texts of the plugin.
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('PresentationPlugin', 'Presentation', 'name singular'),
|
'singular': translate('PresentationPlugin', 'Presentation', 'name singular'),
|
||||||
'plural': translate('PresentationPlugin', 'Presentations', 'name plural')
|
'plural': translate('PresentationPlugin', 'Presentations', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('PresentationPlugin', 'Presentations', 'container title')
|
'title': translate('PresentationPlugin', 'Presentations', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.')
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -92,21 +108,23 @@ class RemotesPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('RemotePlugin', 'Remote', 'name singular'),
|
'singular': translate('RemotePlugin', 'Remote', 'name singular'),
|
||||||
'plural': translate('RemotePlugin', 'Remotes', 'name plural')
|
'plural': translate('RemotePlugin', 'Remotes', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('RemotePlugin', 'Remote', 'container title')
|
'title': translate('RemotePlugin', 'Remote', 'container title')
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
|
@ -31,6 +31,7 @@ The duplicate song removal logic for OpenLP.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
|
@ -45,6 +46,18 @@ from openlp.plugins.songs.lib.songcompare import songs_probably_equal
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def song_generator(songs):
|
||||||
|
"""
|
||||||
|
This is a generator function to return tuples of two songs. When completed then all songs have once been returned
|
||||||
|
combined with any other songs.
|
||||||
|
|
||||||
|
:param songs: All songs in the database.
|
||||||
|
"""
|
||||||
|
for outer_song_counter in range(len(songs) - 1):
|
||||||
|
for inner_song_counter in range(outer_song_counter + 1, len(songs)):
|
||||||
|
yield (songs[outer_song_counter], songs[inner_song_counter])
|
||||||
|
|
||||||
|
|
||||||
class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
|
class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
|
||||||
"""
|
"""
|
||||||
This is the Duplicate Song Removal Wizard. It provides functionality to search for and remove duplicate songs
|
This is the Duplicate Song Removal Wizard. It provides functionality to search for and remove duplicate songs
|
||||||
|
@ -167,24 +180,31 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
|
||||||
max_progress_count = max_songs * (max_songs - 1) // 2
|
max_progress_count = max_songs * (max_songs - 1) // 2
|
||||||
self.duplicate_search_progress_bar.setMaximum(max_progress_count)
|
self.duplicate_search_progress_bar.setMaximum(max_progress_count)
|
||||||
songs = self.plugin.manager.get_all_objects(Song)
|
songs = self.plugin.manager.get_all_objects(Song)
|
||||||
for outer_song_counter in range(max_songs - 1):
|
# Create a worker/process pool to check the songs.
|
||||||
for inner_song_counter in range(outer_song_counter + 1, max_songs):
|
process_number = max(1, multiprocessing.cpu_count() - 1)
|
||||||
if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]):
|
pool = multiprocessing.Pool(process_number)
|
||||||
duplicate_added = self.add_duplicates_to_song_list(
|
result = pool.imap_unordered(songs_probably_equal, song_generator(songs), 30)
|
||||||
songs[outer_song_counter], songs[inner_song_counter])
|
# Do not accept any further tasks. Also this closes the processes if all tasks are done.
|
||||||
if duplicate_added:
|
pool.close()
|
||||||
self.found_duplicates_edit.appendPlainText(
|
# While the processes are still working, start to look at the results.
|
||||||
songs[outer_song_counter].title + " = " + songs[inner_song_counter].title)
|
for song_tuple in result:
|
||||||
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
|
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
|
||||||
# The call to process_events() will keep the GUI responsive.
|
# The call to process_events() will keep the GUI responsive.
|
||||||
self.application.process_events()
|
self.application.process_events()
|
||||||
if self.break_search:
|
if self.break_search:
|
||||||
|
pool.terminate()
|
||||||
return
|
return
|
||||||
|
if song_tuple is None:
|
||||||
|
continue
|
||||||
|
song1, song2 = song_tuple
|
||||||
|
duplicate_added = self.add_duplicates_to_song_list(song1, song2)
|
||||||
|
if duplicate_added:
|
||||||
|
self.found_duplicates_edit.appendPlainText(song1.title + " = " + song2.title)
|
||||||
self.review_total_count = len(self.duplicate_song_list)
|
self.review_total_count = len(self.duplicate_song_list)
|
||||||
if self.review_total_count == 0:
|
if self.duplicate_song_list:
|
||||||
self.notify_no_duplicates()
|
|
||||||
else:
|
|
||||||
self.button(QtGui.QWizard.NextButton).show()
|
self.button(QtGui.QWizard.NextButton).show()
|
||||||
|
else:
|
||||||
|
self.notify_no_duplicates()
|
||||||
finally:
|
finally:
|
||||||
self.application.set_normal_cursor()
|
self.application.set_normal_cursor()
|
||||||
elif page_id == self.review_page_id:
|
elif page_id == self.review_page_id:
|
||||||
|
@ -217,12 +237,12 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
|
||||||
duplicate_added = False
|
duplicate_added = False
|
||||||
for duplicate_group in self.duplicate_song_list:
|
for duplicate_group in self.duplicate_song_list:
|
||||||
# Skip the first song in the duplicate lists, since the first one has to be an earlier song.
|
# Skip the first song in the duplicate lists, since the first one has to be an earlier song.
|
||||||
if search_song in duplicate_group and not duplicate_song in duplicate_group:
|
if search_song in duplicate_group and duplicate_song not in duplicate_group:
|
||||||
duplicate_group.append(duplicate_song)
|
duplicate_group.append(duplicate_song)
|
||||||
duplicate_group_found = True
|
duplicate_group_found = True
|
||||||
duplicate_added = True
|
duplicate_added = True
|
||||||
break
|
break
|
||||||
elif not search_song in duplicate_group and duplicate_song in duplicate_group:
|
elif search_song not in duplicate_group and duplicate_song in duplicate_group:
|
||||||
duplicate_group.append(search_song)
|
duplicate_group.append(search_song)
|
||||||
duplicate_group_found = True
|
duplicate_group_found = True
|
||||||
duplicate_added = True
|
duplicate_added = True
|
||||||
|
@ -244,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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -682,7 +690,6 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
|
||||||
verse_index = VerseType.from_loose_input(verse_name)
|
verse_index = VerseType.from_loose_input(verse_name)
|
||||||
verse_tag = VerseType.tags[verse_index]
|
verse_tag = VerseType.tags[verse_index]
|
||||||
# Later we need to handle v1a as well.
|
# Later we need to handle v1a as well.
|
||||||
#regex = re.compile(r'(\d+\w.)')
|
|
||||||
regex = re.compile(r'\D*(\d+)\D*')
|
regex = re.compile(r'\D*(\d+)\D*')
|
||||||
match = regex.match(verse_num)
|
match = regex.match(verse_num)
|
||||||
if match:
|
if match:
|
||||||
|
@ -906,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)
|
||||||
|
|
|
@ -66,8 +66,10 @@ class Ui_MediaFilesDialog(object):
|
||||||
def retranslateUi(self, media_files_dialog):
|
def retranslateUi(self, media_files_dialog):
|
||||||
"""
|
"""
|
||||||
Translate the UI on the fly.
|
Translate the UI on the fly.
|
||||||
|
|
||||||
|
:param media_files_dialog:
|
||||||
"""
|
"""
|
||||||
media_files_dialog.setWindowTitle(translate('SongsPlugin.MediaFilesForm', 'Select Media File(s)'))
|
media_files_dialog.setWindowTitle(translate('SongsPlugin.MediaFilesForm', 'Select Media File(s)'))
|
||||||
self.select_label.setText(translate('SongsPlugin.MediaFilesForm',
|
self.select_label.setText(translate('SongsPlugin.MediaFilesForm', 'Select one or more audio files from the '
|
||||||
'Select one or more audio files from the list below, and click OK to import them '
|
'list below, and click OK to import them '
|
||||||
'into this song.'))
|
'into this song.'))
|
||||||
|
|
|
@ -152,9 +152,9 @@ class SongExportForm(OpenLPWizard):
|
||||||
self.setWindowTitle(translate('SongsPlugin.ExportWizardForm', 'Song Export Wizard'))
|
self.setWindowTitle(translate('SongsPlugin.ExportWizardForm', 'Song Export Wizard'))
|
||||||
self.title_label.setText(WizardStrings.HeaderStyle %
|
self.title_label.setText(WizardStrings.HeaderStyle %
|
||||||
translate('OpenLP.Ui', 'Welcome to the Song Export Wizard'))
|
translate('OpenLP.Ui', 'Welcome to the Song Export Wizard'))
|
||||||
self.information_label.setText(translate('SongsPlugin.ExportWizardForm', 'This wizard will help to'
|
self.information_label.setText(
|
||||||
' export your songs to the open and free <strong>OpenLyrics </strong> worship '
|
translate('SongsPlugin.ExportWizardForm', 'This wizard will help to export your songs to the open and free '
|
||||||
'song format.'))
|
'<strong>OpenLyrics </strong> worship song format.'))
|
||||||
self.available_songs_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Songs'))
|
self.available_songs_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Songs'))
|
||||||
self.available_songs_page.setSubTitle(translate('SongsPlugin.ExportWizardForm',
|
self.available_songs_page.setSubTitle(translate('SongsPlugin.ExportWizardForm',
|
||||||
'Check the songs you want to export.'))
|
'Check the songs you want to export.'))
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -34,13 +34,13 @@ EasyWorship song databases into the current installation database.
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import re
|
import re
|
||||||
|
import zlib
|
||||||
|
|
||||||
from openlp.core.lib import translate
|
from openlp.core.lib import translate
|
||||||
from openlp.plugins.songs.lib import VerseType
|
from openlp.plugins.songs.lib import VerseType
|
||||||
from openlp.plugins.songs.lib import retrieve_windows_encoding, strip_rtf
|
from openlp.plugins.songs.lib import retrieve_windows_encoding, strip_rtf
|
||||||
from .songimport import SongImport
|
from .songimport import SongImport
|
||||||
|
|
||||||
RTF_STRIPPING_REGEX = re.compile(r'\{\\tx[^}]*\}')
|
|
||||||
# regex: at least two newlines, can have spaces between them
|
# regex: at least two newlines, can have spaces between them
|
||||||
SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*')
|
SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*')
|
||||||
NUMBER_REGEX = re.compile(r'[0-9]+')
|
NUMBER_REGEX = re.compile(r'[0-9]+')
|
||||||
|
@ -77,9 +77,121 @@ class EasyWorshipSongImport(SongImport):
|
||||||
|
|
||||||
def do_import(self):
|
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
|
# Open the DB and MB files if they exist
|
||||||
import_source_mb = self.import_source.replace('.DB', '.MB')
|
import_source_mb = self.import_source.replace('.DB', '.MB')
|
||||||
|
@ -176,7 +288,6 @@ class EasyWorshipSongImport(SongImport):
|
||||||
ccli = self.get_field(fi_ccli)
|
ccli = self.get_field(fi_ccli)
|
||||||
authors = self.get_field(fi_author)
|
authors = self.get_field(fi_author)
|
||||||
words = self.get_field(fi_words)
|
words = self.get_field(fi_words)
|
||||||
# Set the SongImport object members.
|
|
||||||
if copy:
|
if copy:
|
||||||
self.copyright = copy.decode()
|
self.copyright = copy.decode()
|
||||||
if admin:
|
if admin:
|
||||||
|
@ -186,15 +297,35 @@ class EasyWorshipSongImport(SongImport):
|
||||||
'Administered by %s') % admin.decode()
|
'Administered by %s') % admin.decode()
|
||||||
if ccli:
|
if ccli:
|
||||||
self.ccli_number = ccli.decode()
|
self.ccli_number = ccli.decode()
|
||||||
|
if authors:
|
||||||
|
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():
|
||||||
|
self.log_error(self.import_source)
|
||||||
|
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:
|
if authors:
|
||||||
# Split up the authors
|
# Split up the authors
|
||||||
author_list = authors.split(b'/')
|
author_list = authors.split('/')
|
||||||
if len(author_list) < 2:
|
if len(author_list) < 2:
|
||||||
author_list = authors.split(b';')
|
author_list = authors.split(';')
|
||||||
if len(author_list) < 2:
|
if len(author_list) < 2:
|
||||||
author_list = authors.split(b',')
|
author_list = authors.split(',')
|
||||||
for author_name in author_list:
|
for author_name in author_list:
|
||||||
self.add_author(author_name.decode().strip())
|
self.add_author(author_name.strip())
|
||||||
if words:
|
if words:
|
||||||
# Format the lyrics
|
# Format the lyrics
|
||||||
result = strip_rtf(words.decode(), self.encoding)
|
result = strip_rtf(words.decode(), self.encoding)
|
||||||
|
@ -236,19 +367,12 @@ class EasyWorshipSongImport(SongImport):
|
||||||
if len(self.comments) > 5:
|
if len(self.comments) > 5:
|
||||||
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
|
||||||
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
'\n[above are Song Tags with notes imported from EasyWorship]'))
|
||||||
if self.stop_import_flag:
|
|
||||||
break
|
|
||||||
if not self.finish():
|
|
||||||
self.log_error(self.import_source)
|
|
||||||
db_file.close()
|
|
||||||
self.memo_file.close()
|
|
||||||
|
|
||||||
def find_field(self, field_name):
|
def find_field(self, field_name):
|
||||||
"""
|
"""
|
||||||
Find a field in the descriptions
|
Find a field in the descriptions
|
||||||
|
|
||||||
:param field_name: field to find
|
:param field_name: field to find
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
|
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
|
Extract the field
|
||||||
|
|
||||||
:param field_desc_index: Field index value
|
:param field_desc_index: Field index value
|
||||||
:return:
|
:return: The field value
|
||||||
"""
|
"""
|
||||||
field = self.fields[field_desc_index]
|
field = self.fields[field_desc_index]
|
||||||
field_desc = self.field_descriptions[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)
|
return self.memo_file.read(blob_size)
|
||||||
else:
|
else:
|
||||||
return 0
|
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
|
CCLI = 3
|
||||||
DreamBeam = 4
|
DreamBeam = 4
|
||||||
EasySlides = 5
|
EasySlides = 5
|
||||||
EasyWorship = 6
|
EasyWorshipDB = 6
|
||||||
FoilPresenter = 7
|
EasyWorshipService = 7
|
||||||
MediaShout = 8
|
FoilPresenter = 8
|
||||||
OpenSong = 9
|
MediaShout = 9
|
||||||
PowerSong = 10
|
OpenSong = 10
|
||||||
SongBeamer = 11
|
PowerSong = 11
|
||||||
SongPro = 12
|
SongBeamer = 12
|
||||||
SongShowPlus = 13
|
SongPro = 13
|
||||||
SongsOfFellowship = 14
|
SongShowPlus = 14
|
||||||
SundayPlus = 15
|
SongsOfFellowship = 15
|
||||||
WordsOfWorship = 16
|
SundayPlus = 16
|
||||||
WorshipCenterPro = 17
|
WordsOfWorship = 17
|
||||||
ZionWorx = 18
|
WorshipCenterPro = 18
|
||||||
|
ZionWorx = 19
|
||||||
|
|
||||||
# Set optional attribute defaults
|
# Set optional attribute defaults
|
||||||
__defaults__ = {
|
__defaults__ = {
|
||||||
|
@ -224,13 +225,20 @@ class SongFormat(object):
|
||||||
'selectMode': SongFormatSelect.SingleFile,
|
'selectMode': SongFormatSelect.SingleFile,
|
||||||
'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File')
|
'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File')
|
||||||
},
|
},
|
||||||
EasyWorship: {
|
EasyWorshipDB: {
|
||||||
'class': EasyWorshipSongImport,
|
'class': EasyWorshipSongImport,
|
||||||
'name': 'EasyWorship',
|
'name': 'EasyWorship Song Database',
|
||||||
'prefix': 'ew',
|
'prefix': 'ew',
|
||||||
'selectMode': SongFormatSelect.SingleFile,
|
'selectMode': SongFormatSelect.SingleFile,
|
||||||
'filter': '%s (*.db)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Song Database')
|
'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: {
|
FoilPresenter: {
|
||||||
'class': FoilPresenterImport,
|
'class': FoilPresenterImport,
|
||||||
'name': 'Foilpresenter',
|
'name': 'Foilpresenter',
|
||||||
|
@ -341,7 +349,8 @@ class SongFormat(object):
|
||||||
SongFormat.CCLI,
|
SongFormat.CCLI,
|
||||||
SongFormat.DreamBeam,
|
SongFormat.DreamBeam,
|
||||||
SongFormat.EasySlides,
|
SongFormat.EasySlides,
|
||||||
SongFormat.EasyWorship,
|
SongFormat.EasyWorshipDB,
|
||||||
|
SongFormat.EasyWorshipService,
|
||||||
SongFormat.FoilPresenter,
|
SongFormat.FoilPresenter,
|
||||||
SongFormat.MediaShout,
|
SongFormat.MediaShout,
|
||||||
SongFormat.OpenSong,
|
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.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
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class SongMediaItem(MediaManagerItem):
|
||||||
|
|
||||||
def add_end_header_bar(self):
|
def add_end_header_bar(self):
|
||||||
self.toolbar.addSeparator()
|
self.toolbar.addSeparator()
|
||||||
## Song Maintenance Button ##
|
# Song Maintenance Button
|
||||||
self.maintenance_action = self.toolbar.add_toolbar_action('maintenance_action',
|
self.maintenance_action = self.toolbar.add_toolbar_action('maintenance_action',
|
||||||
icon=':/songs/song_maintenance.png',
|
icon=':/songs/song_maintenance.png',
|
||||||
triggers=self.on_song_maintenance_click)
|
triggers=self.on_song_maintenance_click)
|
||||||
|
@ -192,7 +192,7 @@ class SongMediaItem(MediaManagerItem):
|
||||||
song_number = False
|
song_number = False
|
||||||
if not search_results:
|
if not search_results:
|
||||||
search_keywords = search_keywords.rpartition(' ')
|
search_keywords = search_keywords.rpartition(' ')
|
||||||
search_string = '%' + search_keywords + '%'
|
search_string = '%' + search_keywords[0] + '%'
|
||||||
search_results = self.plugin.manager.get_all_objects(Book,
|
search_results = self.plugin.manager.get_all_objects(Book,
|
||||||
Book.name.like(search_string), Book.name.asc())
|
Book.name.like(search_string), Book.name.asc())
|
||||||
song_number = re.sub(r'[^0-9]', '', search_keywords[2])
|
song_number = re.sub(r'[^0-9]', '', search_keywords[2])
|
||||||
|
@ -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)
|
||||||
|
@ -266,7 +265,7 @@ class SongMediaItem(MediaManagerItem):
|
||||||
# Do not display temporary songs
|
# Do not display temporary songs
|
||||||
if song.temporary:
|
if song.temporary:
|
||||||
continue
|
continue
|
||||||
if song_number and not song_number in song.song_number:
|
if song_number and song_number not in song.song_number:
|
||||||
continue
|
continue
|
||||||
song_detail = '%s - %s (%s)' % (book.name, song.song_number, song.title)
|
song_detail = '%s - %s (%s)' % (book.name, song.song_number, song.title)
|
||||||
song_name = QtGui.QListWidgetItem(song_detail)
|
song_name = QtGui.QListWidgetItem(song_detail)
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -53,7 +53,7 @@ class OpenLyricsImport(SongImport):
|
||||||
Initialise the Open Lyrics importer.
|
Initialise the Open Lyrics importer.
|
||||||
"""
|
"""
|
||||||
log.debug('initialise OpenLyricsImport')
|
log.debug('initialise OpenLyricsImport')
|
||||||
SongImport.__init__(self, manager, **kwargs)
|
super(OpenLyricsImport, self).__init__(manager, **kwargs)
|
||||||
self.open_lyrics = OpenLyrics(self.manager)
|
self.open_lyrics = OpenLyrics(self.manager)
|
||||||
|
|
||||||
def do_import(self):
|
def do_import(self):
|
||||||
|
@ -69,7 +69,7 @@ class OpenLyricsImport(SongImport):
|
||||||
try:
|
try:
|
||||||
# Pass a file object, because lxml does not cope with some
|
# Pass a file object, because lxml does not cope with some
|
||||||
# special characters in the path (see lp:757673 and lp:744337).
|
# special characters in the path (see lp:757673 and lp:744337).
|
||||||
parsed_file = etree.parse(open(file_path, 'r'), parser)
|
parsed_file = etree.parse(open(file_path, 'rb'), parser)
|
||||||
xml = etree.tostring(parsed_file).decode()
|
xml = etree.tostring(parsed_file).decode()
|
||||||
self.open_lyrics.xml_to_song(xml)
|
self.open_lyrics.xml_to_song(xml)
|
||||||
except etree.XMLSyntaxError:
|
except etree.XMLSyntaxError:
|
||||||
|
|
|
@ -56,11 +56,15 @@ class SongBeamerTypes(object):
|
||||||
'Zwischenspiel': VerseType.tags[VerseType.Bridge],
|
'Zwischenspiel': VerseType.tags[VerseType.Bridge],
|
||||||
'Pre-Chorus': VerseType.tags[VerseType.PreChorus],
|
'Pre-Chorus': VerseType.tags[VerseType.PreChorus],
|
||||||
'Pre-Refrain': VerseType.tags[VerseType.PreChorus],
|
'Pre-Refrain': VerseType.tags[VerseType.PreChorus],
|
||||||
|
'Misc': VerseType.tags[VerseType.Other],
|
||||||
'Pre-Bridge': VerseType.tags[VerseType.Other],
|
'Pre-Bridge': VerseType.tags[VerseType.Other],
|
||||||
'Pre-Coda': VerseType.tags[VerseType.Other],
|
'Pre-Coda': VerseType.tags[VerseType.Other],
|
||||||
|
'Part': VerseType.tags[VerseType.Other],
|
||||||
|
'Teil': VerseType.tags[VerseType.Other],
|
||||||
'Unbekannt': VerseType.tags[VerseType.Other],
|
'Unbekannt': VerseType.tags[VerseType.Other],
|
||||||
'Unknown': VerseType.tags[VerseType.Other],
|
'Unknown': VerseType.tags[VerseType.Other],
|
||||||
'Unbenannt': VerseType.tags[VerseType.Other]
|
'Unbenannt': VerseType.tags[VerseType.Other],
|
||||||
|
'$$M=': VerseType.tags[VerseType.Other]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,7 +136,8 @@ class SongBeamerImport(SongImport):
|
||||||
line = str(line).strip()
|
line = str(line).strip()
|
||||||
if line.startswith('#') and not read_verses:
|
if line.startswith('#') and not read_verses:
|
||||||
self.parseTags(line)
|
self.parseTags(line)
|
||||||
elif line.startswith('---'):
|
elif line.startswith('--'):
|
||||||
|
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
|
||||||
if self.current_verse:
|
if self.current_verse:
|
||||||
self.replace_html_tags()
|
self.replace_html_tags()
|
||||||
self.add_verse(self.current_verse, self.current_verse_type)
|
self.add_verse(self.current_verse, self.current_verse_type)
|
||||||
|
@ -282,4 +287,7 @@ class SongBeamerImport(SongImport):
|
||||||
if marks[1].isdigit():
|
if marks[1].isdigit():
|
||||||
self.current_verse_type += marks[1]
|
self.current_verse_type += marks[1]
|
||||||
return True
|
return True
|
||||||
|
elif marks[0].startswith('$$M='): # this verse-mark cannot be numbered
|
||||||
|
self.current_verse_type = SongBeamerTypes.MarkTypes['$$M=']
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -52,13 +52,13 @@ MIN_BLOCK_SIZE = 70
|
||||||
MAX_TYPO_SIZE = 3
|
MAX_TYPO_SIZE = 3
|
||||||
|
|
||||||
|
|
||||||
def songs_probably_equal(song1, song2):
|
def songs_probably_equal(song_tupel):
|
||||||
"""
|
"""
|
||||||
Calculate and return whether two songs are probably equal.
|
Calculate and return whether two songs are probably equal.
|
||||||
|
|
||||||
:param song1: The first song to compare.
|
:param song_tupel: A tuple of two songs to compare.
|
||||||
:param song2: The second song to compare.
|
|
||||||
"""
|
"""
|
||||||
|
song1, song2 = song_tupel
|
||||||
if len(song1.search_lyrics) < len(song2.search_lyrics):
|
if len(song1.search_lyrics) < len(song2.search_lyrics):
|
||||||
small = song1.search_lyrics
|
small = song1.search_lyrics
|
||||||
large = song2.search_lyrics
|
large = song2.search_lyrics
|
||||||
|
@ -75,18 +75,19 @@ def songs_probably_equal(song1, song2):
|
||||||
for element in diff_no_typos:
|
for element in diff_no_typos:
|
||||||
if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE:
|
if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE:
|
||||||
length_of_equal_blocks += _op_length(element)
|
length_of_equal_blocks += _op_length(element)
|
||||||
|
|
||||||
if length_of_equal_blocks >= MIN_BLOCK_SIZE:
|
if length_of_equal_blocks >= MIN_BLOCK_SIZE:
|
||||||
return True
|
return song1, song2
|
||||||
# Check 2: Similarity based on the relative length of the longest equal block.
|
# Check 2: Similarity based on the relative length of the longest equal block.
|
||||||
# Calculate the length of the largest equal block of the diff set.
|
# Calculate the length of the largest equal block of the diff set.
|
||||||
length_of_longest_equal_block = 0
|
length_of_longest_equal_block = 0
|
||||||
for element in diff_no_typos:
|
for element in diff_no_typos:
|
||||||
if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block:
|
if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block:
|
||||||
length_of_longest_equal_block = _op_length(element)
|
length_of_longest_equal_block = _op_length(element)
|
||||||
if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 // 3:
|
if length_of_longest_equal_block > len(small) * 2 // 3:
|
||||||
return True
|
return song1, song2
|
||||||
# Both checks failed. We assume the songs are not equal.
|
# Both checks failed. We assume the songs are not equal.
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _op_length(opcode):
|
def _op_length(opcode):
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -675,6 +687,7 @@ class OpenLyrics(object):
|
||||||
sxml = SongXML()
|
sxml = SongXML()
|
||||||
verses = {}
|
verses = {}
|
||||||
verse_def_list = []
|
verse_def_list = []
|
||||||
|
verse_order = self._text(properties.verseOrder).split(' ') if hasattr(properties, 'verseOrder') else []
|
||||||
try:
|
try:
|
||||||
lyrics = song_xml.lyrics
|
lyrics = song_xml.lyrics
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -717,13 +730,17 @@ class OpenLyrics(object):
|
||||||
else:
|
else:
|
||||||
verses[(verse_tag, verse_number, lang, translit, verse_part)] = text
|
verses[(verse_tag, verse_number, lang, translit, verse_part)] = text
|
||||||
verse_def_list.append((verse_tag, verse_number, lang, translit, verse_part))
|
verse_def_list.append((verse_tag, verse_number, lang, translit, verse_part))
|
||||||
|
# Update verse order when the verse name has changed
|
||||||
|
if verse_def != verse_tag + verse_number + verse_part:
|
||||||
|
for i in range(len(verse_order)):
|
||||||
|
if verse_order[i] == verse_def:
|
||||||
|
verse_order[i] = verse_tag + verse_number + verse_part
|
||||||
# We have to use a list to keep the order, as dicts are not sorted.
|
# We have to use a list to keep the order, as dicts are not sorted.
|
||||||
for verse in verse_def_list:
|
for verse in verse_def_list:
|
||||||
sxml.add_verse_to_lyrics(verse[0], verse[1], verses[verse], verse[2])
|
sxml.add_verse_to_lyrics(verse[0], verse[1], verses[verse], verse[2])
|
||||||
song_obj.lyrics = str(sxml.extract_xml(), 'utf-8')
|
song_obj.lyrics = str(sxml.extract_xml(), 'utf-8')
|
||||||
# Process verse order
|
# Process verse order
|
||||||
if hasattr(properties, 'verseOrder'):
|
song_obj.verse_order = ' '.join(verse_order)
|
||||||
song_obj.verse_order = self._text(properties.verseOrder)
|
|
||||||
|
|
||||||
def _process_songbooks(self, properties, song):
|
def _process_songbooks(self, properties, song):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -132,7 +132,7 @@ class SongsPlugin(Plugin):
|
||||||
)
|
)
|
||||||
import_menu.addAction(self.import_songselect_item)
|
import_menu.addAction(self.import_songselect_item)
|
||||||
|
|
||||||
def add_export_menu_Item(self, export_menu):
|
def add_export_menu_item(self, export_menu):
|
||||||
"""
|
"""
|
||||||
Give the Songs plugin the opportunity to add items to the **Export** menu.
|
Give the Songs plugin the opportunity to add items to the **Export** menu.
|
||||||
|
|
||||||
|
@ -261,12 +261,12 @@ class SongsPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('SongsPlugin', 'Song', 'name singular'),
|
'singular': translate('SongsPlugin', 'Song', 'name singular'),
|
||||||
'plural': translate('SongsPlugin', 'Songs', 'name plural')
|
'plural': translate('SongsPlugin', 'Songs', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('SongsPlugin', 'Songs', 'container title')
|
'title': translate('SongsPlugin', 'Songs', 'container title')
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,12 +246,12 @@ class SongUsagePlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
Called to define all translatable texts of the plugin
|
Called to define all translatable texts of the plugin
|
||||||
"""
|
"""
|
||||||
## Name PluginList ##
|
# Name PluginList
|
||||||
self.text_strings[StringContent.Name] = {
|
self.text_strings[StringContent.Name] = {
|
||||||
'singular': translate('SongUsagePlugin', 'SongUsage', 'name singular'),
|
'singular': translate('SongUsagePlugin', 'SongUsage', 'name singular'),
|
||||||
'plural': translate('SongUsagePlugin', 'SongUsage', 'name plural')
|
'plural': translate('SongUsagePlugin', 'SongUsage', 'name plural')
|
||||||
}
|
}
|
||||||
## Name for MediaDockManager, SettingsManager ##
|
# Name for MediaDockManager, SettingsManager
|
||||||
self.text_strings[StringContent.VisibleName] = {
|
self.text_strings[StringContent.VisibleName] = {
|
||||||
'title': translate('SongUsagePlugin', 'SongUsage', 'container title')
|
'title': translate('SongUsagePlugin', 'SongUsage', 'container title')
|
||||||
}
|
}
|
||||||
|
|
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 |
|
@ -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>
|
||||||
|
|
|
@ -40,6 +40,7 @@ You can look up the token in the Branch-01-Pull job configuration or ask in IRC.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
import re
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
import sys
|
import sys
|
||||||
|
@ -49,6 +50,9 @@ from jenkins import Jenkins
|
||||||
|
|
||||||
|
|
||||||
JENKINS_URL = 'http://ci.openlp.org/'
|
JENKINS_URL = 'http://ci.openlp.org/'
|
||||||
|
REPO_REGEX = r'(.*/+)(~.*)'
|
||||||
|
# Allows us to black list token. So when we change the token, we can display a proper message to the user.
|
||||||
|
OLD_TOKENS = []
|
||||||
|
|
||||||
|
|
||||||
class OpenLPJobs(object):
|
class OpenLPJobs(object):
|
||||||
|
@ -59,9 +63,20 @@ class OpenLPJobs(object):
|
||||||
Branch_Functional = 'Branch-02-Functional-Tests'
|
Branch_Functional = 'Branch-02-Functional-Tests'
|
||||||
Branch_Interface = 'Branch-03-Interface-Tests'
|
Branch_Interface = 'Branch-03-Interface-Tests'
|
||||||
Branch_Windows = 'Branch-04-Windows_Tests'
|
Branch_Windows = 'Branch-04-Windows_Tests'
|
||||||
Branch_PEP = 'Branch-05-Code-Analysis'
|
Branch_PEP = 'Branch-05a-Code_Analysis'
|
||||||
|
Branch_Coverage = 'Branch-05b-Test_Coverage'
|
||||||
|
|
||||||
Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows, Branch_PEP]
|
Jobs = [Branch_Pull, Branch_Functional, Branch_Interface, Branch_Windows, Branch_PEP, Branch_Coverage]
|
||||||
|
|
||||||
|
|
||||||
|
class Colour(object):
|
||||||
|
"""
|
||||||
|
This class holds values which can be used to print coloured text.
|
||||||
|
"""
|
||||||
|
RED_START = '\033[1;31m'
|
||||||
|
RED_END = '\033[1;m'
|
||||||
|
GREEN_START = '\033[1;32m'
|
||||||
|
GREEN_END = '\033[1;m'
|
||||||
|
|
||||||
|
|
||||||
class JenkinsTrigger(object):
|
class JenkinsTrigger(object):
|
||||||
|
@ -79,14 +94,25 @@ class JenkinsTrigger(object):
|
||||||
"""
|
"""
|
||||||
Ask our jenkins server to build the "Branch-01-Pull" job.
|
Ask our jenkins server to build the "Branch-01-Pull" job.
|
||||||
"""
|
"""
|
||||||
self.jenkins_instance.job(OpenLPJobs.Branch_Pull).build({'BRANCH_NAME': self.repo_name}, token=self.token)
|
bzr = Popen(('bzr', 'whoami'), stdout=PIPE, stderr=PIPE)
|
||||||
|
raw_output, error = bzr.communicate()
|
||||||
|
# We just want the name (not the email).
|
||||||
|
name = ' '.join(raw_output.decode().split()[:-1])
|
||||||
|
cause = 'Build triggered by %s (%s)' % (name, self.repo_name)
|
||||||
|
self.jenkins_instance.job(OpenLPJobs.Branch_Pull).build(
|
||||||
|
{'BRANCH_NAME': self.repo_name, 'cause': cause}, token=self.token)
|
||||||
|
|
||||||
def print_output(self):
|
def print_output(self):
|
||||||
"""
|
"""
|
||||||
Print the status information of the build tirggered.
|
Print the status information of the build tirggered.
|
||||||
"""
|
"""
|
||||||
print("Add this to your merge proposal:")
|
print('Add this to your merge proposal:')
|
||||||
print("--------------------------------")
|
print('--------------------------------')
|
||||||
|
bzr = Popen(('bzr', 'revno'), stdout=PIPE, stderr=PIPE)
|
||||||
|
raw_output, error = bzr.communicate()
|
||||||
|
revno = raw_output.decode().strip()
|
||||||
|
print('%s (revision %s)' % (get_repo_name(), revno))
|
||||||
|
|
||||||
for job in OpenLPJobs.Jobs:
|
for job in OpenLPJobs.Jobs:
|
||||||
self.__print_build_info(job)
|
self.__print_build_info(job)
|
||||||
|
|
||||||
|
@ -107,22 +133,22 @@ class JenkinsTrigger(object):
|
||||||
"""
|
"""
|
||||||
job = self.jenkins_instance.job(job_name)
|
job = self.jenkins_instance.job(job_name)
|
||||||
while job.info['inQueue']:
|
while job.info['inQueue']:
|
||||||
# Give other processes the possibility to take over. Like Thread.yield().
|
time.sleep(1)
|
||||||
time.sleep(0)
|
|
||||||
build = job.last_build
|
build = job.last_build
|
||||||
build.wait()
|
build.wait()
|
||||||
result_string = build.info['result']
|
if build.info['result'] == 'SUCCESS':
|
||||||
|
# Make 'SUCCESS' green.
|
||||||
|
result_string = '%s%s%s' % (Colour.GREEN_START, build.info['result'], Colour.GREEN_END)
|
||||||
|
else:
|
||||||
|
# Make 'FAILURE' red.
|
||||||
|
result_string = '%s%s%s' % (Colour.RED_START, build.info['result'], Colour.RED_END)
|
||||||
url = build.info['url']
|
url = build.info['url']
|
||||||
print('[%s] %s' % (result_string, url))
|
print('[%s] %s' % (result_string, url))
|
||||||
# On failure open the browser.
|
|
||||||
#if result_string == "FAILURE":
|
|
||||||
# url += 'console'
|
|
||||||
# Popen(('xdg-open', url), stderr=PIPE)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -139,46 +165,41 @@ def get_repo_name():
|
||||||
for line in output_list:
|
for line in output_list:
|
||||||
# Check if it is remote branch.
|
# Check if it is remote branch.
|
||||||
if 'push branch' in line:
|
if 'push branch' in line:
|
||||||
repo_name = line.replace('push branch: bzr+ssh://bazaar.launchpad.net/', 'lp:')
|
match = re.match(REPO_REGEX, line)
|
||||||
|
if match:
|
||||||
|
repo_name = 'lp:%s' % match.group(2)
|
||||||
break
|
break
|
||||||
elif 'checkout of branch' in line:
|
elif 'checkout of branch' in line:
|
||||||
repo_name = line.replace('checkout of branch: bzr+ssh://bazaar.launchpad.net/', 'lp:')
|
match = re.match(REPO_REGEX, line)
|
||||||
|
if match:
|
||||||
|
repo_name = 'lp:%s' % match.group(2)
|
||||||
break
|
break
|
||||||
repo_name = repo_name.strip('/')
|
return repo_name.strip('/')
|
||||||
|
|
||||||
# Did we find the branch name?
|
|
||||||
if not repo_name:
|
|
||||||
for line in output_list:
|
|
||||||
# Check if the branch was pushed.
|
|
||||||
if 'Shared repository with trees (format: 2a)' in line:
|
|
||||||
print('Not a branch. cd to a branch.')
|
|
||||||
return
|
|
||||||
print('Not a branch. Have you pushed it to launchpad?')
|
|
||||||
return
|
|
||||||
return repo_name
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
usage = 'Usage: python %prog TOKEN [options]'
|
usage = 'Usage: python %prog TOKEN [options]'
|
||||||
|
|
||||||
parser = OptionParser(usage=usage)
|
parser = OptionParser(usage=usage)
|
||||||
parser.add_option('-d', '--disable-output', dest='enable_output', action="store_false", default=True,
|
parser.add_option('-d', '--disable-output', dest='enable_output', action='store_false', default=True,
|
||||||
help='Disable output.')
|
help='Disable output.')
|
||||||
parser.add_option('-b', '--open-browser', dest='open_browser', action="store_true", default=False,
|
parser.add_option('-b', '--open-browser', dest='open_browser', action='store_true', default=False,
|
||||||
help='Opens the jenkins page in your browser.')
|
help='Opens the jenkins page in your browser.')
|
||||||
#parser.add_option('-e', '--open-browser-on-error', dest='open_browser_on_error', action="store_true",
|
|
||||||
# default=False, help='Opens the jenkins page in your browser in case a test fails.')
|
|
||||||
options, args = parser.parse_args(sys.argv)
|
options, args = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
if len(args) == 2:
|
if len(args) == 2:
|
||||||
if not get_repo_name():
|
if not get_repo_name():
|
||||||
|
print('Not a branch. Have you pushed it to launchpad? Did you cd to the branch?')
|
||||||
return
|
return
|
||||||
token = args[-1]
|
token = args[-1]
|
||||||
|
if token in OLD_TOKENS:
|
||||||
|
print('Your token is not valid anymore. Get the most recent one.')
|
||||||
|
return
|
||||||
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.
|
||||||
if options.open_browser:
|
if options.open_browser:
|
||||||
|
|
|
@ -96,7 +96,7 @@ class CommandStack(object):
|
||||||
return len(self.data)
|
return len(self.data)
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def __getitem__(self, index):
|
||||||
if not index in self.data:
|
if index not in self.data:
|
||||||
return None
|
return None
|
||||||
elif self.data[index].get('arguments'):
|
elif self.data[index].get('arguments'):
|
||||||
return self.data[index]['command'], self.data[index]['arguments']
|
return self.data[index]['command'], self.data[index]['arguments']
|
||||||
|
|
|
@ -32,12 +32,13 @@ Functional tests to test the AppLocation class and related methods.
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from openlp.core.common import de_hump
|
from openlp.core.common import de_hump, trace_error_handler
|
||||||
|
from tests.functional import MagicMock, patch
|
||||||
|
|
||||||
|
|
||||||
class TestInitFunctions(TestCase):
|
class TestCommonFunctions(TestCase):
|
||||||
"""
|
"""
|
||||||
A test suite to test out various functions in the __init__ class.
|
A test suite to test out various functions in the openlp.core.common module.
|
||||||
"""
|
"""
|
||||||
def de_hump_conversion_test(self):
|
def de_hump_conversion_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -64,3 +65,19 @@ class TestInitFunctions(TestCase):
|
||||||
|
|
||||||
# THEN: the new string should be converted to python format
|
# THEN: the new string should be converted to python format
|
||||||
self.assertTrue(new_string == "my_class", 'The class name should have been preserved')
|
self.assertTrue(new_string == "my_class", 'The class name should have been preserved')
|
||||||
|
|
||||||
|
def trace_error_handler_test(self):
|
||||||
|
"""
|
||||||
|
Test the trace_error_handler() method
|
||||||
|
"""
|
||||||
|
# GIVEN: Mocked out objects
|
||||||
|
with patch('openlp.core.common.traceback') as mocked_traceback:
|
||||||
|
mocked_traceback.extract_stack.return_value = [('openlp.fake', 56, None, 'trace_error_handler_test')]
|
||||||
|
mocked_logger = MagicMock()
|
||||||
|
|
||||||
|
# WHEN: trace_error_handler() is called
|
||||||
|
trace_error_handler(mocked_logger)
|
||||||
|
|
||||||
|
# THEN: The mocked_logger.error() method should have been called with the correct parameters
|
||||||
|
mocked_logger.error.assert_called_with(
|
||||||
|
'OpenLP Error trace\n File openlp.fake at line 56 \n\t called trace_error_handler_test')
|
|
@ -52,7 +52,7 @@ class TestRegistryProperties(TestCase, RegistryProperties):
|
||||||
# GIVEN an Empty Registry
|
# GIVEN an Empty Registry
|
||||||
# WHEN there is no Application
|
# WHEN there is no Application
|
||||||
# THEN the application should be none
|
# THEN the application should be none
|
||||||
self.assertEquals(self.application, None, 'The application value should be None')
|
self.assertEqual(self.application, None, 'The application value should be None')
|
||||||
|
|
||||||
def application_test(self):
|
def application_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -63,4 +63,4 @@ class TestRegistryProperties(TestCase, RegistryProperties):
|
||||||
# WHEN the application is registered
|
# WHEN the application is registered
|
||||||
Registry().register('application', application)
|
Registry().register('application', application)
|
||||||
# THEN the application should be none
|
# THEN the application should be none
|
||||||
self.assertEquals(self.application, application, 'The application value should match')
|
self.assertEqual(self.application, application, 'The application value should match')
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
|
@ -30,12 +30,16 @@
|
||||||
Package to test the openlp.core.ui package.
|
Package to test the openlp.core.ui package.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
|
|
||||||
from openlp.core.common import Registry
|
from openlp.core.common import Registry
|
||||||
from openlp.core.lib import ImageManager, ScreenList
|
from openlp.core.lib import ImageManager, ScreenList
|
||||||
|
from openlp.core.lib.imagemanager import Priority
|
||||||
|
from tests.functional import patch
|
||||||
from tests.helpers.testmixin import TestMixin
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
|
||||||
|
@ -51,6 +55,8 @@ class TestImageManager(TestCase, TestMixin):
|
||||||
self.get_application()
|
self.get_application()
|
||||||
ScreenList.create(self.app.desktop())
|
ScreenList.create(self.app.desktop())
|
||||||
self.image_manager = ImageManager()
|
self.image_manager = ImageManager()
|
||||||
|
self.lock = Lock()
|
||||||
|
self.sleep_time = 0.1
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""
|
"""
|
||||||
|
@ -82,3 +88,87 @@ class TestImageManager(TestCase, TestMixin):
|
||||||
with self.assertRaises(KeyError) as context:
|
with self.assertRaises(KeyError) as context:
|
||||||
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
|
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
|
||||||
self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
|
self.assertNotEquals(context.exception, '', 'KeyError exception should have been thrown for missing image')
|
||||||
|
|
||||||
|
def process_cache_test(self):
|
||||||
|
"""
|
||||||
|
Test the process_cache method
|
||||||
|
"""
|
||||||
|
with patch('openlp.core.lib.imagemanager.resize_image') as mocked_resize_image, \
|
||||||
|
patch('openlp.core.lib.imagemanager.image_to_byte') as mocked_image_to_byte:
|
||||||
|
# GIVEN: Mocked functions
|
||||||
|
mocked_resize_image.side_effect = self.mocked_resize_image
|
||||||
|
mocked_image_to_byte.side_effect = self.mocked_image_to_byte
|
||||||
|
image1 = 'church.jpg'
|
||||||
|
image2 = 'church2.jpg'
|
||||||
|
image3 = 'church3.jpg'
|
||||||
|
image4 = 'church4.jpg'
|
||||||
|
|
||||||
|
# WHEN: Add the images. Then get the lock (=queue can not be processed).
|
||||||
|
self.lock.acquire()
|
||||||
|
self.image_manager.add_image(TEST_PATH, image1, None)
|
||||||
|
self.image_manager.add_image(TEST_PATH, image2, None)
|
||||||
|
|
||||||
|
# THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
|
||||||
|
# is being processed (see mocked methods/functions).
|
||||||
|
# Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
|
||||||
|
# priority is adjusted to Priority.Lowest).
|
||||||
|
self.assertEqual(self.get_image_priority(image1), Priority.Normal,
|
||||||
|
"image1's priority should be 'Priority.Normal'")
|
||||||
|
self.assertEqual(self.get_image_priority(image2), Priority.Normal,
|
||||||
|
"image2's priority should be 'Priority.Normal'")
|
||||||
|
|
||||||
|
# WHEN: Add more images.
|
||||||
|
self.image_manager.add_image(TEST_PATH, image3, None)
|
||||||
|
self.image_manager.add_image(TEST_PATH, image4, None)
|
||||||
|
# Allow the queue to process.
|
||||||
|
self.lock.release()
|
||||||
|
# Request some "data".
|
||||||
|
image_bytes = self.image_manager.get_image_bytes(TEST_PATH, image4)
|
||||||
|
image_object = self.image_manager.get_image(TEST_PATH, image3)
|
||||||
|
# Now the mocked methods/functions do not have to sleep anymore.
|
||||||
|
self.sleep_time = 0
|
||||||
|
# Wait for the queue to finish.
|
||||||
|
while not self.image_manager._conversion_queue.empty():
|
||||||
|
time.sleep(0.1)
|
||||||
|
# Because empty() is not reliable, wait a litte; just to make sure.
|
||||||
|
time.sleep(0.1)
|
||||||
|
# THEN: The images' priority reflect how they were processed.
|
||||||
|
self.assertEqual(self.image_manager._conversion_queue.qsize(), 0, "The queue should be empty.")
|
||||||
|
self.assertEqual(self.get_image_priority(image1), Priority.Lowest,
|
||||||
|
"The image should have not been requested (=Lowest)")
|
||||||
|
self.assertEqual(self.get_image_priority(image2), Priority.Lowest,
|
||||||
|
"The image should have not been requested (=Lowest)")
|
||||||
|
self.assertEqual(self.get_image_priority(image3), Priority.Low,
|
||||||
|
"Only the QImage should have been requested (=Low).")
|
||||||
|
self.assertEqual(self.get_image_priority(image4), Priority.Urgent,
|
||||||
|
"The image bytes should have been requested (=Urgent).")
|
||||||
|
|
||||||
|
def get_image_priority(self, image):
|
||||||
|
"""
|
||||||
|
This is a help method to get the priority of the given image out of the image_manager's cache.
|
||||||
|
|
||||||
|
NOTE: This requires, that the image has been added to the image manager using the *TEST_PATH*.
|
||||||
|
|
||||||
|
:param image: The name of the image. E. g. ``image1``
|
||||||
|
"""
|
||||||
|
return self.image_manager._cache[(TEST_PATH, image)].priority
|
||||||
|
|
||||||
|
def mocked_resize_image(self, *args):
|
||||||
|
"""
|
||||||
|
This is a mocked method, so that we can control the work flow of the image manager.
|
||||||
|
"""
|
||||||
|
self.lock.acquire()
|
||||||
|
self.lock.release()
|
||||||
|
# The sleep time is adjusted in the test case.
|
||||||
|
time.sleep(self.sleep_time)
|
||||||
|
return QtGui.QImage()
|
||||||
|
|
||||||
|
def mocked_image_to_byte(self, *args):
|
||||||
|
"""
|
||||||
|
This is a mocked method, so that we can control the work flow of the image manager.
|
||||||
|
"""
|
||||||
|
self.lock.acquire()
|
||||||
|
self.lock.release()
|
||||||
|
# The sleep time is adjusted in the test case.
|
||||||
|
time.sleep(self.sleep_time)
|
||||||
|
return ''
|
|
@ -212,9 +212,9 @@ class TestPluginManager(TestCase):
|
||||||
# WHEN: We run hook_export_menu()
|
# WHEN: We run hook_export_menu()
|
||||||
plugin_manager.hook_export_menu()
|
plugin_manager.hook_export_menu()
|
||||||
|
|
||||||
# THEN: The add_export_menu_Item() method should not have been called
|
# THEN: The add_export_menu_item() method should not have been called
|
||||||
self.assertEqual(0, mocked_plugin.add_export_menu_Item.call_count,
|
self.assertEqual(0, mocked_plugin.add_export_menu_item.call_count,
|
||||||
'The add_export_menu_Item() method should not have been called.')
|
'The add_export_menu_item() method should not have been called.')
|
||||||
|
|
||||||
def hook_export_menu_with_active_plugin_test(self):
|
def hook_export_menu_with_active_plugin_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -229,8 +229,8 @@ class TestPluginManager(TestCase):
|
||||||
# WHEN: We run hook_export_menu()
|
# WHEN: We run hook_export_menu()
|
||||||
plugin_manager.hook_export_menu()
|
plugin_manager.hook_export_menu()
|
||||||
|
|
||||||
# THEN: The add_export_menu_Item() method should have been called
|
# THEN: The add_export_menu_item() method should have been called
|
||||||
mocked_plugin.add_export_menu_Item.assert_called_with(self.mocked_main_window.file_export_menu)
|
mocked_plugin.add_export_menu_item.assert_called_with(self.mocked_main_window.file_export_menu)
|
||||||
|
|
||||||
def hook_upgrade_plugin_settings_with_disabled_plugin_test(self):
|
def hook_upgrade_plugin_settings_with_disabled_plugin_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -264,7 +264,7 @@ class TestPluginManager(TestCase):
|
||||||
# WHEN: We run hook_upgrade_plugin_settings()
|
# WHEN: We run hook_upgrade_plugin_settings()
|
||||||
plugin_manager.hook_upgrade_plugin_settings(settings)
|
plugin_manager.hook_upgrade_plugin_settings(settings)
|
||||||
|
|
||||||
# THEN: The add_export_menu_Item() method should have been called
|
# THEN: The add_export_menu_item() method should have been called
|
||||||
mocked_plugin.upgrade_settings.assert_called_with(settings)
|
mocked_plugin.upgrade_settings.assert_called_with(settings)
|
||||||
|
|
||||||
def hook_tools_menu_with_disabled_plugin_test(self):
|
def hook_tools_menu_with_disabled_plugin_test(self):
|
||||||
|
|
|
@ -49,7 +49,7 @@ class TestRenderer(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
Set up the components need for all tests.
|
Set up the components need for all tests
|
||||||
"""
|
"""
|
||||||
# Mocked out desktop object
|
# Mocked out desktop object
|
||||||
self.desktop = MagicMock()
|
self.desktop = MagicMock()
|
||||||
|
@ -67,7 +67,7 @@ class TestRenderer(TestCase):
|
||||||
|
|
||||||
def initial_renderer_test(self):
|
def initial_renderer_test(self):
|
||||||
"""
|
"""
|
||||||
Test the initial renderer state .
|
Test the initial renderer state
|
||||||
"""
|
"""
|
||||||
# GIVEN: A new renderer instance.
|
# GIVEN: A new renderer instance.
|
||||||
renderer = Renderer()
|
renderer = Renderer()
|
||||||
|
@ -77,7 +77,7 @@ class TestRenderer(TestCase):
|
||||||
|
|
||||||
def default_screen_layout_test(self):
|
def default_screen_layout_test(self):
|
||||||
"""
|
"""
|
||||||
Test the default layout calculations.
|
Test the default layout calculations
|
||||||
"""
|
"""
|
||||||
# GIVEN: A new renderer instance.
|
# GIVEN: A new renderer instance.
|
||||||
renderer = Renderer()
|
renderer = Renderer()
|
||||||
|
@ -87,3 +87,35 @@ class TestRenderer(TestCase):
|
||||||
self.assertEqual(renderer.height, 768, 'The base renderer should be a live controller')
|
self.assertEqual(renderer.height, 768, 'The base renderer should be a live controller')
|
||||||
self.assertEqual(renderer.screen_ratio, 0.75, 'The base renderer should be a live controller')
|
self.assertEqual(renderer.screen_ratio, 0.75, 'The base renderer should be a live controller')
|
||||||
self.assertEqual(renderer.footer_start, 691, 'The base renderer should be a live controller')
|
self.assertEqual(renderer.footer_start, 691, 'The base renderer should be a live controller')
|
||||||
|
|
||||||
|
def _get_start_tags_test(self):
|
||||||
|
"""
|
||||||
|
Test the _get_start_tags() method
|
||||||
|
"""
|
||||||
|
# GIVEN: A new renderer instance. Broken raw_text (missing closing tags).
|
||||||
|
renderer = Renderer()
|
||||||
|
given_raw_text = '{st}{r}Text text text'
|
||||||
|
expected_tuple = ('{st}{r}Text text text{/r}{/st}', '{st}{r}',
|
||||||
|
'<strong><span style="-webkit-text-fill-color:red">')
|
||||||
|
|
||||||
|
# WHEN:
|
||||||
|
result = renderer._get_start_tags(given_raw_text)
|
||||||
|
|
||||||
|
# THEN: Check if the correct tuple is returned.
|
||||||
|
self.assertEqual(result, expected_tuple), 'A tuple should be returned containing the text with correct ' \
|
||||||
|
'tags, the opening tags, and the opening html tags.'
|
||||||
|
|
||||||
|
def _word_split_test(self):
|
||||||
|
"""
|
||||||
|
Test the _word_split() method
|
||||||
|
"""
|
||||||
|
# GIVEN: A line of text
|
||||||
|
renderer = Renderer()
|
||||||
|
given_line = 'beginning asdf \n end asdf'
|
||||||
|
expected_words = ['beginning', 'asdf', 'end', 'asdf']
|
||||||
|
|
||||||
|
# WHEN: Split the line
|
||||||
|
result_words = renderer._words_split(given_line)
|
||||||
|
|
||||||
|
# THEN: The word lists should be the same.
|
||||||
|
self.assertListEqual(result_words, expected_words)
|
||||||
|
|
|
@ -82,6 +82,38 @@ class TestUi(TestCase):
|
||||||
self.assertEqual(1, len(btnbox.buttons()))
|
self.assertEqual(1, len(btnbox.buttons()))
|
||||||
self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0]))
|
self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0]))
|
||||||
|
|
||||||
|
def test_create_button(self):
|
||||||
|
"""
|
||||||
|
Test creating a button
|
||||||
|
"""
|
||||||
|
# GIVEN: A dialog
|
||||||
|
dialog = QtGui.QDialog()
|
||||||
|
|
||||||
|
# WHEN: We create the button
|
||||||
|
btn = create_button(dialog, 'my_btn')
|
||||||
|
|
||||||
|
# THEN: We should get a button with a name
|
||||||
|
self.assertIsInstance(btn, QtGui.QPushButton)
|
||||||
|
self.assertEqual('my_btn', btn.objectName())
|
||||||
|
self.assertTrue(btn.isEnabled())
|
||||||
|
|
||||||
|
# WHEN: We create a button with some attributes
|
||||||
|
btn = create_button(dialog, 'my_btn', text='Hello', tooltip='How are you?', enabled=False)
|
||||||
|
|
||||||
|
# THEN: We should get a button with those attributes
|
||||||
|
self.assertIsInstance(btn, QtGui.QPushButton)
|
||||||
|
self.assertEqual('Hello', btn.text())
|
||||||
|
self.assertEqual('How are you?', btn.toolTip())
|
||||||
|
self.assertFalse(btn.isEnabled())
|
||||||
|
|
||||||
|
# WHEN: We create a toolbutton
|
||||||
|
btn = create_button(dialog, 'my_btn', btn_class='toolbutton')
|
||||||
|
|
||||||
|
# THEN: We should get a toolbutton
|
||||||
|
self.assertIsInstance(btn, QtGui.QToolButton)
|
||||||
|
self.assertEqual('my_btn', btn.objectName())
|
||||||
|
self.assertTrue(btn.isEnabled())
|
||||||
|
|
||||||
def test_create_valign_selection_widgets(self):
|
def test_create_valign_selection_widgets(self):
|
||||||
"""
|
"""
|
||||||
Test creating a combo box for valign selection
|
Test creating a combo box for valign selection
|
||||||
|
@ -98,3 +130,43 @@ class TestUi(TestCase):
|
||||||
self.assertEqual(combo, label.buddy())
|
self.assertEqual(combo, label.buddy())
|
||||||
for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
|
for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
|
||||||
self.assertTrue(combo.findText(text) >= 0)
|
self.assertTrue(combo.findText(text) >= 0)
|
||||||
|
|
||||||
|
def test_create_horizontal_adjusting_combo_box(self):
|
||||||
|
"""
|
||||||
|
Test creating a horizontal adjusting combo box
|
||||||
|
"""
|
||||||
|
# GIVEN: A dialog
|
||||||
|
dialog = QtGui.QDialog()
|
||||||
|
|
||||||
|
# WHEN: We create the combobox
|
||||||
|
combo = create_horizontal_adjusting_combo_box(dialog, 'combo1')
|
||||||
|
|
||||||
|
# THEN: We should get a ComboBox
|
||||||
|
self.assertIsInstance(combo, QtGui.QComboBox)
|
||||||
|
self.assertEqual('combo1', combo.objectName())
|
||||||
|
self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy())
|
||||||
|
|
||||||
|
def test_create_action(self):
|
||||||
|
"""
|
||||||
|
Test creating an action
|
||||||
|
"""
|
||||||
|
# GIVEN: A dialog
|
||||||
|
dialog = QtGui.QDialog()
|
||||||
|
|
||||||
|
# WHEN: We create an action
|
||||||
|
action = create_action(dialog, 'my_action')
|
||||||
|
|
||||||
|
# THEN: We should get a QAction
|
||||||
|
self.assertIsInstance(action, QtGui.QAction)
|
||||||
|
self.assertEqual('my_action', action.objectName())
|
||||||
|
|
||||||
|
# WHEN: We create an action with some properties
|
||||||
|
action = create_action(dialog, 'my_action', text='my text', icon=':/wizards/wizard_firsttime.bmp',
|
||||||
|
tooltip='my tooltip', statustip='my statustip')
|
||||||
|
|
||||||
|
# THEN: These properties should be set
|
||||||
|
self.assertIsInstance(action, QtGui.QAction)
|
||||||
|
self.assertEqual('my text', action.text())
|
||||||
|
self.assertIsInstance(action.icon(), QtGui.QIcon)
|
||||||
|
self.assertEqual('my tooltip', action.toolTip())
|
||||||
|
self.assertEqual('my statustip', action.statusTip())
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
# -*- 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.firsttimeform package.
|
||||||
|
"""
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from tests.functional import MagicMock
|
||||||
|
|
||||||
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
from openlp.core.common import Registry
|
||||||
|
from openlp.core.ui.firsttimeform import FirstTimeForm
|
||||||
|
|
||||||
|
|
||||||
|
class TestFirstTimeForm(TestCase, TestMixin):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
screens = MagicMock()
|
||||||
|
self.get_application()
|
||||||
|
Registry.create()
|
||||||
|
Registry().register('application', self.app)
|
||||||
|
self.first_time_form = FirstTimeForm(screens)
|
||||||
|
|
||||||
|
def test_access_to_config(self):
|
||||||
|
"""
|
||||||
|
Test if we can access the First Time Form's config file
|
||||||
|
"""
|
||||||
|
# GIVEN A new First Time Form instance.
|
||||||
|
|
||||||
|
# WHEN The default First Time Form is built.
|
||||||
|
|
||||||
|
# THEN The First Time Form web configuration file should be accessable.
|
||||||
|
self.assertTrue(self.first_time_form.web_access,
|
||||||
|
'First Time Wizard\'s web configuration file should be available')
|
||||||
|
|
||||||
|
def test_parsable_config(self):
|
||||||
|
"""
|
||||||
|
Test if the First Time Form's config file is parsable
|
||||||
|
"""
|
||||||
|
# GIVEN A new First Time Form instance.
|
||||||
|
|
||||||
|
# WHEN The default First Time Form is built.
|
||||||
|
|
||||||
|
# THEN The First Time Form web configuration file should be parsable
|
||||||
|
self.assertTrue(self.first_time_form.songs_url,
|
||||||
|
'First Time Wizard\'s web configuration file should be parsable')
|
|
@ -79,3 +79,31 @@ class TestMainDisplay(TestCase):
|
||||||
|
|
||||||
# THEN: The controller should not be a live controller.
|
# THEN: The controller should not be a live controller.
|
||||||
self.assertEqual(main_display.is_live, True, 'The main display should be a live controller')
|
self.assertEqual(main_display.is_live, True, 'The main display should be a live controller')
|
||||||
|
|
||||||
|
def set_transparency_test(self):
|
||||||
|
"""
|
||||||
|
Test creating an instance of the MainDisplay class
|
||||||
|
"""
|
||||||
|
# GIVEN: get an instance of MainDisplay
|
||||||
|
display = MagicMock()
|
||||||
|
main_display = MainDisplay(display)
|
||||||
|
|
||||||
|
# WHEN: We enable transparency
|
||||||
|
main_display.set_transparency(True)
|
||||||
|
|
||||||
|
# THEN: There should be a Stylesheet
|
||||||
|
self.assertEqual('QGraphicsView {background: transparent; border: 0px;}', main_display.styleSheet(),
|
||||||
|
'MainDisplay instance should be transparent')
|
||||||
|
self.assertFalse(main_display.autoFillBackground(),
|
||||||
|
'MainDisplay instance should be without background auto fill')
|
||||||
|
self.assertTrue(main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground),
|
||||||
|
'MainDisplay hasnt translucent background')
|
||||||
|
|
||||||
|
# WHEN: We disable transparency
|
||||||
|
main_display.set_transparency(False)
|
||||||
|
|
||||||
|
# THEN: The Stylesheet should be empty
|
||||||
|
self.assertEqual('QGraphicsView {}', main_display.styleSheet(),
|
||||||
|
'MainDisplay instance should not be transparent')
|
||||||
|
self.assertFalse(main_display.testAttribute(QtCore.Qt.WA_TranslucentBackground),
|
||||||
|
'MainDisplay hasnt translucent background')
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
# -*- 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 package.
|
||||||
|
"""
|
||||||
|
from PyQt4 import QtCore
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from openlp.core.ui.media import get_media_players
|
||||||
|
|
||||||
|
from tests.functional import MagicMock, patch
|
||||||
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestMedia(TestCase, TestMixin):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_media_players_no_config(self):
|
||||||
|
"""
|
||||||
|
Test that when there's no config, get_media_players() returns an empty list of players (not a string)
|
||||||
|
"""
|
||||||
|
def value_results(key):
|
||||||
|
if key == 'media/players':
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# GIVEN: A mocked out Settings() object
|
||||||
|
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
|
||||||
|
mocked_value.side_effect = value_results
|
||||||
|
|
||||||
|
# WHEN: get_media_players() is called
|
||||||
|
used_players, overridden_player = get_media_players()
|
||||||
|
|
||||||
|
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
|
||||||
|
self.assertEqual([], used_players, 'Used players should be an empty list')
|
||||||
|
self.assertEqual('', overridden_player, 'Overridden player should be an empty string')
|
||||||
|
|
||||||
|
def test_get_media_players_no_players(self):
|
||||||
|
"""
|
||||||
|
Test that when there's no players but overridden player is set, get_media_players() returns 'auto'
|
||||||
|
"""
|
||||||
|
def value_results(key):
|
||||||
|
if key == 'media/override player':
|
||||||
|
return QtCore.Qt.Checked
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# GIVEN: A mocked out Settings() object
|
||||||
|
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
|
||||||
|
mocked_value.side_effect = value_results
|
||||||
|
|
||||||
|
# WHEN: get_media_players() is called
|
||||||
|
used_players, overridden_player = get_media_players()
|
||||||
|
|
||||||
|
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
|
||||||
|
self.assertEqual([], used_players, 'Used players should be an empty list')
|
||||||
|
self.assertEqual('auto', overridden_player, 'Overridden player should be "auto"')
|
||||||
|
|
||||||
|
def test_get_media_players_with_valid_list(self):
|
||||||
|
"""
|
||||||
|
Test that when get_media_players() is called the string list is interpreted correctly
|
||||||
|
"""
|
||||||
|
def value_results(key):
|
||||||
|
if key == 'media/players':
|
||||||
|
return '[vlc,webkit,phonon]'
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# GIVEN: A mocked out Settings() object
|
||||||
|
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
|
||||||
|
mocked_value.side_effect = value_results
|
||||||
|
|
||||||
|
# WHEN: get_media_players() is called
|
||||||
|
used_players, overridden_player = get_media_players()
|
||||||
|
|
||||||
|
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
|
||||||
|
self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct')
|
||||||
|
self.assertEqual('', overridden_player, 'Overridden player should be an empty string')
|
||||||
|
|
||||||
|
def test_get_media_players_with_overridden_player(self):
|
||||||
|
"""
|
||||||
|
Test that when get_media_players() is called the overridden player is correctly set
|
||||||
|
"""
|
||||||
|
def value_results(key):
|
||||||
|
if key == 'media/players':
|
||||||
|
return '[vlc,webkit,phonon]'
|
||||||
|
else:
|
||||||
|
return QtCore.Qt.Checked
|
||||||
|
|
||||||
|
# GIVEN: A mocked out Settings() object
|
||||||
|
with patch('openlp.core.ui.media.Settings.value') as mocked_value:
|
||||||
|
mocked_value.side_effect = value_results
|
||||||
|
|
||||||
|
# WHEN: get_media_players() is called
|
||||||
|
used_players, overridden_player = get_media_players()
|
||||||
|
|
||||||
|
# THEN: the used_players should be an empty list, and the overridden player should be an empty string
|
||||||
|
self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct')
|
||||||
|
self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players')
|
|
@ -35,9 +35,107 @@ from PyQt4 import QtGui, QtCore
|
||||||
|
|
||||||
from openlp.core.common import Settings
|
from openlp.core.common import Settings
|
||||||
from openlp.core.utils import ActionList
|
from openlp.core.utils import ActionList
|
||||||
|
from openlp.core.utils.actions import CategoryActionList
|
||||||
|
from tests.functional import MagicMock
|
||||||
from tests.helpers.testmixin import TestMixin
|
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):
|
class TestActionList(TestCase, TestMixin):
|
||||||
"""
|
"""
|
||||||
Test the ActionList class
|
Test the ActionList class
|
||||||
|
|
|
@ -180,4 +180,4 @@ class TestBSExtract(TestCase):
|
||||||
'http://m.bibleserver.com/overlay/selectBook?translation=NIV')
|
'http://m.bibleserver.com/overlay/selectBook?translation=NIV')
|
||||||
self.assertFalse(self.mock_log.error.called, 'log.error should not have been called')
|
self.assertFalse(self.mock_log.error.called, 'log.error should not have been called')
|
||||||
self.assertFalse(self.mock_send_error_message.called, 'send_error_message should not have been called')
|
self.assertFalse(self.mock_send_error_message.called, 'send_error_message should not have been called')
|
||||||
self.assertEquals(result, ['Genesis', 'Leviticus'])
|
self.assertEqual(result, ['Genesis', 'Leviticus'])
|
||||||
|
|
|
@ -69,6 +69,20 @@ SONG_TEST_DATA = [
|
||||||
'Just to bow and receive a new blessing,\nIn the beautiful garden of prayer.', 'v3')],
|
'Just to bow and receive a new blessing,\nIn the beautiful garden of prayer.', 'v3')],
|
||||||
'verse_order_list': []}]
|
'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):
|
class EasyWorshipSongImportLogger(EasyWorshipSongImport):
|
||||||
"""
|
"""
|
||||||
|
@ -139,10 +153,10 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
|
|
||||||
# THEN:
|
# THEN:
|
||||||
self.assertIsNotNone(field_desc_entry, 'Import should not be none')
|
self.assertIsNotNone(field_desc_entry, 'Import should not be none')
|
||||||
self.assertEquals(field_desc_entry.name, name, 'FieldDescEntry.name should be the same as the name argument')
|
self.assertEqual(field_desc_entry.name, name, 'FieldDescEntry.name should be the same as the name argument')
|
||||||
self.assertEquals(field_desc_entry.field_type, field_type,
|
self.assertEqual(field_desc_entry.field_type, field_type,
|
||||||
'FieldDescEntry.type should be the same as the type argument')
|
'FieldDescEntry.type should be the same as the type argument')
|
||||||
self.assertEquals(field_desc_entry.size, size, 'FieldDescEntry.size should be the same as the size argument')
|
self.assertEqual(field_desc_entry.size, size, 'FieldDescEntry.size should be the same as the size argument')
|
||||||
|
|
||||||
def create_importer_test(self):
|
def create_importer_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -174,7 +188,7 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
for field_name in existing_fields:
|
for field_name in existing_fields:
|
||||||
|
|
||||||
# THEN: The item corresponding the index returned should have the same name attribute
|
# THEN: The item corresponding the index returned should have the same name attribute
|
||||||
self.assertEquals(importer.field_descriptions[importer.find_field(field_name)].name, field_name)
|
self.assertEqual(importer.field_descriptions[importer.find_field(field_name)].name, field_name)
|
||||||
|
|
||||||
def find_non_existing_field_test(self):
|
def find_non_existing_field_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -230,7 +244,7 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
return_value = importer.get_field(field_index)
|
return_value = importer.get_field(field_index)
|
||||||
|
|
||||||
# THEN: get_field should return the known results
|
# THEN: get_field should return the known results
|
||||||
self.assertEquals(return_value, result,
|
self.assertEqual(return_value, result,
|
||||||
'get_field should return "%s" when called with "%s"' %
|
'get_field should return "%s" when called with "%s"' %
|
||||||
(result, TEST_FIELDS[field_index]))
|
(result, TEST_FIELDS[field_index]))
|
||||||
|
|
||||||
|
@ -257,7 +271,7 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
get_field_seek_calls = test_results[2]['seek']
|
get_field_seek_calls = test_results[2]['seek']
|
||||||
|
|
||||||
# THEN: get_field should return the appropriate value with the appropriate mocked objects being called
|
# THEN: get_field should return the appropriate value with the appropriate mocked objects being called
|
||||||
self.assertEquals(importer.get_field(field_index), get_field_result)
|
self.assertEqual(importer.get_field(field_index), get_field_result)
|
||||||
for call in get_field_read_calls:
|
for call in get_field_read_calls:
|
||||||
mocked_memo_file.read.assert_any_call(call)
|
mocked_memo_file.read.assert_any_call(call)
|
||||||
for call in get_field_seek_calls:
|
for call in get_field_seek_calls:
|
||||||
|
@ -357,9 +371,9 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
|
self.assertIsNone(importer.do_import(), 'do_import should return None when db_size is less than 0x800')
|
||||||
mocked_retrieve_windows_encoding.assert_call(encoding)
|
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",
|
# 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
|
# WHEN: Importing each file
|
||||||
importer.import_source = os.path.join(TEST_PATH, 'Songs.DB')
|
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
|
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||||
# called.
|
# 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:
|
for song_data in SONG_TEST_DATA:
|
||||||
title = song_data['title']
|
title = song_data['title']
|
||||||
author_calls = song_data['authors']
|
author_calls = song_data['authors']
|
||||||
|
@ -403,11 +418,52 @@ class TestEasyWorshipSongImport(TestCase):
|
||||||
if song_copyright:
|
if song_copyright:
|
||||||
self.assertEqual(importer.copyright, song_copyright)
|
self.assertEqual(importer.copyright, song_copyright)
|
||||||
if ccli_number:
|
if ccli_number:
|
||||||
self.assertEquals(importer.ccli_number, ccli_number, 'ccli_number for %s should be %s'
|
self.assertEqual(importer.ccli_number, ccli_number,
|
||||||
% (title, ccli_number))
|
'ccli_number for %s should be %s' % (title, ccli_number))
|
||||||
for verse_text, verse_tag in add_verse_calls:
|
for verse_text, verse_tag in add_verse_calls:
|
||||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||||
if verse_order_list:
|
if verse_order_list:
|
||||||
self.assertEquals(importer.verse_order_list, verse_order_list,
|
self.assertEqual(importer.verse_order_list, verse_order_list,
|
||||||
'verse_order_list for %s should be %s' % (title, verse_order_list))
|
'verse_order_list for %s should be %s' % (title, verse_order_list))
|
||||||
mocked_finish.assert_called_with()
|
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()
|
||||||
|
|
|
@ -96,10 +96,10 @@ class TestLib(TestCase):
|
||||||
self.song2.search_lyrics = self.full_lyrics
|
self.song2.search_lyrics = self.full_lyrics
|
||||||
|
|
||||||
# WHEN: We compare those songs for equality.
|
# WHEN: We compare those songs for equality.
|
||||||
result = songs_probably_equal(self.song1, self.song2)
|
result = songs_probably_equal((self.song1, self.song2))
|
||||||
|
|
||||||
# THEN: The result should be True.
|
# THEN: The result should be a tuple..
|
||||||
assert result is True, 'The result should be True'
|
assert result == (self.song1, self.song2), 'The result should be the tuble of songs'
|
||||||
|
|
||||||
def songs_probably_equal_short_song_test(self):
|
def songs_probably_equal_short_song_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -110,10 +110,10 @@ class TestLib(TestCase):
|
||||||
self.song2.search_lyrics = self.short_lyrics
|
self.song2.search_lyrics = self.short_lyrics
|
||||||
|
|
||||||
# WHEN: We compare those songs for equality.
|
# WHEN: We compare those songs for equality.
|
||||||
result = songs_probably_equal(self.song1, self.song2)
|
result = songs_probably_equal((self.song1, self.song2))
|
||||||
|
|
||||||
# THEN: The result should be True.
|
# THEN: The result should be a tuple..
|
||||||
assert result is True, 'The result should be True'
|
assert result == (self.song1, self.song2), 'The result should be the tuble of songs'
|
||||||
|
|
||||||
def songs_probably_equal_error_song_test(self):
|
def songs_probably_equal_error_song_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -124,10 +124,10 @@ class TestLib(TestCase):
|
||||||
self.song2.search_lyrics = self.error_lyrics
|
self.song2.search_lyrics = self.error_lyrics
|
||||||
|
|
||||||
# WHEN: We compare those songs for equality.
|
# WHEN: We compare those songs for equality.
|
||||||
result = songs_probably_equal(self.song1, self.song2)
|
result = songs_probably_equal((self.song1, self.song2))
|
||||||
|
|
||||||
# THEN: The result should be True.
|
# THEN: The result should be a tuple of songs..
|
||||||
assert result is True, 'The result should be True'
|
assert result == (self.song1, self.song2), 'The result should be the tuble of songs'
|
||||||
|
|
||||||
def songs_probably_equal_different_song_test(self):
|
def songs_probably_equal_different_song_test(self):
|
||||||
"""
|
"""
|
||||||
|
@ -138,10 +138,10 @@ class TestLib(TestCase):
|
||||||
self.song2.search_lyrics = self.different_lyrics
|
self.song2.search_lyrics = self.different_lyrics
|
||||||
|
|
||||||
# WHEN: We compare those songs for equality.
|
# WHEN: We compare those songs for equality.
|
||||||
result = songs_probably_equal(self.song1, self.song2)
|
result = songs_probably_equal((self.song1, self.song2))
|
||||||
|
|
||||||
# THEN: The result should be False.
|
# THEN: The result should be None.
|
||||||
assert result is False, 'The result should be False'
|
assert result is None, 'The result should be None'
|
||||||
|
|
||||||
def remove_typos_beginning_test(self):
|
def remove_typos_beginning_test(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- 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 #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
This module contains tests for the OpenLyrics song importer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from tests.functional import MagicMock, patch
|
||||||
|
from openlp.plugins.songs.lib.openlyricsimport import OpenLyricsImport
|
||||||
|
from openlp.plugins.songs.lib.songimport import SongImport
|
||||||
|
|
||||||
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
|
'..', '..', '..', 'resources', 'openlyricssongs'))
|
||||||
|
SONG_TEST_DATA = {
|
||||||
|
'What a friend we have in Jesus.xml': {
|
||||||
|
'title': 'What A Friend We Have In Jesus',
|
||||||
|
'verses': [
|
||||||
|
('What a friend we have in Jesus, All ours sins and griefs to bear;\n\
|
||||||
|
What a privilege to carry, Everything to God in prayer!\n\
|
||||||
|
O what peace we often forfeit, O what needless pain we bear;\n\
|
||||||
|
All because we do not carry, Everything to God in prayer!', 'v1'),
|
||||||
|
('Have we trials and temptations? Is there trouble anywhere?\n\
|
||||||
|
We should never be discouraged, Take it to the Lord in prayer.\n\
|
||||||
|
Can we find a friend so faithful? Who will all our sorrows share?\n\
|
||||||
|
Jesus knows our every weakness; Take it to the Lord in prayer.', 'v2'),
|
||||||
|
('Are we weak and heavy laden, Cumbered with a load of care?\n\
|
||||||
|
Precious Saviour still our refuge; Take it to the Lord in prayer.\n\
|
||||||
|
Do thy friends despise forsake thee? Take it to the Lord in prayer!\n\
|
||||||
|
In His arms He’ll take and shield thee; Thou wilt find a solace there.', 'v3')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenLyricsImport(TestCase):
|
||||||
|
"""
|
||||||
|
Test the functions in the :mod:`openlyricsimport` module.
|
||||||
|
"""
|
||||||
|
def create_importer_test(self):
|
||||||
|
"""
|
||||||
|
Test creating an instance of the OpenLyrics file importer
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||||
|
with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'):
|
||||||
|
mocked_manager = MagicMock()
|
||||||
|
|
||||||
|
# WHEN: An importer object is created
|
||||||
|
importer = OpenLyricsImport(mocked_manager, filenames=[])
|
||||||
|
|
||||||
|
# THEN: The importer should be an instance of SongImport
|
||||||
|
self.assertIsInstance(importer, SongImport)
|
||||||
|
|
||||||
|
def file_import_test(self):
|
||||||
|
"""
|
||||||
|
Test the actual import of real song files
|
||||||
|
"""
|
||||||
|
# GIVEN: Test files with a mocked out "manager" and a mocked out "import_wizard"
|
||||||
|
for song_file in SONG_TEST_DATA:
|
||||||
|
mocked_manager = MagicMock()
|
||||||
|
mocked_import_wizard = MagicMock()
|
||||||
|
importer = OpenLyricsImport(mocked_manager, filenames=[])
|
||||||
|
importer.import_wizard = mocked_import_wizard
|
||||||
|
importer.open_lyrics = MagicMock()
|
||||||
|
importer.open_lyrics.xml_to_song = MagicMock()
|
||||||
|
|
||||||
|
# WHEN: Importing each file
|
||||||
|
importer.import_source = [os.path.join(TEST_PATH, song_file)]
|
||||||
|
importer.do_import()
|
||||||
|
|
||||||
|
# THEN: The xml_to_song() method should have been called
|
||||||
|
self.assertTrue(importer.open_lyrics.xml_to_song.called)
|
|
@ -35,6 +35,7 @@ from unittest import TestCase
|
||||||
|
|
||||||
from tests.functional import MagicMock, patch
|
from tests.functional import MagicMock, patch
|
||||||
from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
|
from openlp.plugins.songs.lib.songbeamerimport import SongBeamerImport
|
||||||
|
from openlp.plugins.songs.lib import VerseType
|
||||||
|
|
||||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
'..', '..', '..', 'resources', 'songbeamersongs'))
|
'..', '..', '..', 'resources', 'songbeamersongs'))
|
||||||
|
@ -48,7 +49,8 @@ SONG_TEST_DATA = {
|
||||||
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
|
('4. Lobsingt seiner Treu´,\ndie immerdar neu,\nbis Er uns zur Herrlichket führet!\n\n', 'v')
|
||||||
],
|
],
|
||||||
'song_book_name': 'Glaubenslieder I',
|
'song_book_name': 'Glaubenslieder I',
|
||||||
'song_number': "1"
|
'song_number': "1",
|
||||||
|
'authors': ['Carl Brockhaus', 'Johann Jakob Vetter']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +91,7 @@ class TestSongBeamerImport(TestCase):
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
# THEN: do_import should return none and the progress bar maximum should not be set.
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
||||||
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||||
'setMaxium on import_wizard.progress_bar should not have been called')
|
'setMaxium on import_wizard.progress_bar should not have been called')
|
||||||
|
|
||||||
def valid_import_source_test(self):
|
def valid_import_source_test(self):
|
||||||
|
@ -139,17 +141,98 @@ class TestSongBeamerImport(TestCase):
|
||||||
add_verse_calls = SONG_TEST_DATA[song_file]['verses']
|
add_verse_calls = SONG_TEST_DATA[song_file]['verses']
|
||||||
song_book_name = SONG_TEST_DATA[song_file]['song_book_name']
|
song_book_name = SONG_TEST_DATA[song_file]['song_book_name']
|
||||||
song_number = SONG_TEST_DATA[song_file]['song_number']
|
song_number = SONG_TEST_DATA[song_file]['song_number']
|
||||||
|
song_authors = SONG_TEST_DATA[song_file]['authors']
|
||||||
|
|
||||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||||
# called.
|
# called.
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
|
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
|
||||||
self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (song_file, title))
|
self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (song_file, title))
|
||||||
for verse_text, verse_tag in add_verse_calls:
|
for verse_text, verse_tag in add_verse_calls:
|
||||||
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||||
if song_book_name:
|
if song_book_name:
|
||||||
self.assertEquals(importer.song_book_name, song_book_name, 'song_book_name for %s should be "%s"' %
|
self.assertEqual(importer.song_book_name, song_book_name,
|
||||||
(song_file, song_book_name))
|
'song_book_name for %s should be "%s"' % (song_file, song_book_name))
|
||||||
if song_number:
|
if song_number:
|
||||||
self.assertEquals(importer.song_number, song_number, 'song_number for %s should be %s' %
|
self.assertEqual(importer.song_number, song_number,
|
||||||
(song_file, song_number))
|
'song_number for %s should be %s' % (song_file, song_number))
|
||||||
|
if song_authors:
|
||||||
|
for author in importer.authors:
|
||||||
|
self.assertIn(author, song_authors)
|
||||||
mocked_finish.assert_called_with()
|
mocked_finish.assert_called_with()
|
||||||
|
|
||||||
|
def check_verse_marks_test(self):
|
||||||
|
"""
|
||||||
|
Tests different lines to see if a verse mark is detected or not
|
||||||
|
"""
|
||||||
|
|
||||||
|
# GIVEN: line with unnumbered verse-type
|
||||||
|
line = 'Refrain'
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back true and c as self.current_verse_type
|
||||||
|
self.assertTrue(result, 'Versemark for <Refrain> should be found, value true')
|
||||||
|
self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
|
||||||
|
|
||||||
|
# GIVEN: line with unnumbered verse-type and trailing space
|
||||||
|
line = 'Refrain '
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back true and c as self.current_verse_type
|
||||||
|
self.assertTrue(result, 'Versemark for <Refrain > should be found, value true')
|
||||||
|
self.assertEqual(self.current_verse_type, 'c', '<Refrain > should be interpreted as <c>')
|
||||||
|
|
||||||
|
# GIVEN: line with numbered verse-type
|
||||||
|
line = 'Verse 1'
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back true and v1 as self.current_verse_type
|
||||||
|
self.assertTrue(result, 'Versemark for <Verse 1> should be found, value true')
|
||||||
|
self.assertEqual(self.current_verse_type, 'v1', u'<Verse 1> should be interpreted as <v1>')
|
||||||
|
|
||||||
|
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
|
||||||
|
line = '$$M=special'
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back true and o as self.current_verse_type
|
||||||
|
self.assertTrue(result, 'Versemark for <$$M=special> should be found, value true')
|
||||||
|
self.assertEqual(self.current_verse_type, 'o', u'<$$M=special> should be interpreted as <o>')
|
||||||
|
|
||||||
|
# GIVEN: line with song-text with 3 words
|
||||||
|
line = 'Jesus my saviour'
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back false and none as self.current_verse_type
|
||||||
|
self.assertFalse(result, 'No versemark for <Jesus my saviour> should be found, value false')
|
||||||
|
self.assertIsNone(self.current_verse_type, '<Jesus my saviour> should be interpreted as none versemark')
|
||||||
|
|
||||||
|
# GIVEN: line with song-text with 2 words
|
||||||
|
line = 'Praise him'
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back false and none as self.current_verse_type
|
||||||
|
self.assertFalse(result, 'No versemark for <Praise him> should be found, value false')
|
||||||
|
self.assertIsNone(self.current_verse_type, '<Praise him> should be interpreted as none versemark')
|
||||||
|
|
||||||
|
# GIVEN: line with only a space (could occur, nothing regular)
|
||||||
|
line = ' '
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back false and none as self.current_verse_type
|
||||||
|
self.assertFalse(result, 'No versemark for < > should be found, value false')
|
||||||
|
self.assertIsNone(self.current_verse_type, '< > should be interpreted as none versemark')
|
||||||
|
|
||||||
|
# GIVEN: blank line (could occur, nothing regular)
|
||||||
|
line = ''
|
||||||
|
self.current_verse_type = None
|
||||||
|
# WHEN: line is being checked for verse marks
|
||||||
|
result = SongBeamerImport.check_verse_marks(self, line)
|
||||||
|
# THEN: we should get back false and none as self.current_verse_type
|
||||||
|
self.assertFalse(result, 'No versemark for <> should be found, value false')
|
||||||
|
self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark')
|
||||||
|
|
|
@ -95,7 +95,7 @@ class TestSongShowPlusImport(TestCase):
|
||||||
|
|
||||||
# THEN: do_import should return none and the progress bar maximum should not be set.
|
# THEN: do_import should return none and the progress bar maximum should not be set.
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
self.assertIsNone(importer.do_import(), 'do_import should return None when import_source is not a list')
|
||||||
self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
self.assertEqual(mocked_import_wizard.progress_bar.setMaximum.called, False,
|
||||||
'setMaximum on import_wizard.progress_bar should not have been called')
|
'setMaximum on import_wizard.progress_bar should not have been called')
|
||||||
|
|
||||||
def valid_import_source_test(self):
|
def valid_import_source_test(self):
|
||||||
|
@ -143,7 +143,7 @@ class TestSongShowPlusImport(TestCase):
|
||||||
|
|
||||||
# THEN: The returned value should should correlate with the input arguments
|
# THEN: The returned value should should correlate with the input arguments
|
||||||
for original_tag, openlp_tag in test_values:
|
for original_tag, openlp_tag in test_values:
|
||||||
self.assertEquals(importer.to_openlp_verse_tag(original_tag), openlp_tag,
|
self.assertEqual(importer.to_openlp_verse_tag(original_tag), openlp_tag,
|
||||||
'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' %
|
'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' %
|
||||||
(openlp_tag, original_tag))
|
(openlp_tag, original_tag))
|
||||||
|
|
||||||
|
@ -172,6 +172,6 @@ class TestSongShowPlusImport(TestCase):
|
||||||
|
|
||||||
# THEN: The returned value should should correlate with the input arguments
|
# THEN: The returned value should should correlate with the input arguments
|
||||||
for original_tag, openlp_tag in test_values:
|
for original_tag, openlp_tag in test_values:
|
||||||
self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag,
|
self.assertEqual(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag,
|
||||||
'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' %
|
'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"' %
|
||||||
(openlp_tag, original_tag))
|
(openlp_tag, original_tag))
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
# -*- 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 #
|
||||||
|
###############################################################################
|
||||||
|
"""
|
||||||
|
This module contains tests for the ZionWorx song importer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from tests.functional import MagicMock, patch
|
||||||
|
from openlp.plugins.songs.lib.zionworximport import ZionWorxImport
|
||||||
|
from openlp.plugins.songs.lib.songimport import SongImport
|
||||||
|
|
||||||
|
|
||||||
|
class TestZionWorxImport(TestCase):
|
||||||
|
"""
|
||||||
|
Test the functions in the :mod:`zionworximport` module.
|
||||||
|
"""
|
||||||
|
def create_importer_test(self):
|
||||||
|
"""
|
||||||
|
Test creating an instance of the ZionWorx file importer
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked out SongImport class, and a mocked out "manager"
|
||||||
|
with patch('openlp.plugins.songs.lib.songbeamerimport.SongImport'):
|
||||||
|
mocked_manager = MagicMock()
|
||||||
|
|
||||||
|
# WHEN: An importer object is created
|
||||||
|
importer = ZionWorxImport(mocked_manager, filenames=[])
|
||||||
|
|
||||||
|
# THEN: The importer should be an instance of SongImport
|
||||||
|
self.assertIsInstance(importer, SongImport)
|
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- 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.__init__ package.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from PyQt4 import QtCore
|
||||||
|
|
||||||
|
from openlp.core import OpenLP
|
||||||
|
from tests.helpers.testmixin import TestMixin
|
||||||
|
|
||||||
|
|
||||||
|
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'resources'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestInit(TestCase, TestMixin):
|
||||||
|
def setUp(self):
|
||||||
|
with patch('openlp.core.common.OpenLPMixin.__init__') as constructor:
|
||||||
|
constructor.return_value = None
|
||||||
|
self.openlp = OpenLP(list())
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
del self.openlp
|
||||||
|
|
||||||
|
def event_test(self):
|
||||||
|
"""
|
||||||
|
Test the reimplemented event method
|
||||||
|
"""
|
||||||
|
# GIVEN: A file path and a QEvent.
|
||||||
|
file_path = os.path.join(TEST_PATH, 'church.jpg')
|
||||||
|
mocked_file_method = MagicMock(return_value=file_path)
|
||||||
|
event = QtCore.QEvent(QtCore.QEvent.FileOpen)
|
||||||
|
event.file = mocked_file_method
|
||||||
|
|
||||||
|
# WHEN: Call the vent method.
|
||||||
|
result = self.openlp.event(event)
|
||||||
|
|
||||||
|
# THEN: The path should be inserted.
|
||||||
|
self.assertTrue(result, "The method should have returned True.")
|
||||||
|
mocked_file_method.assert_called_once_with()
|
||||||
|
self.assertEqual(self.openlp.args[0], file_path, "The path should be in args.")
|
|
@ -110,30 +110,30 @@ class SongImportTestHelper(TestCase):
|
||||||
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
# THEN: do_import should return none, the song data should be as expected, and finish should have been
|
||||||
# called.
|
# called.
|
||||||
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
|
self.assertIsNone(importer.do_import(), 'do_import should return None when it has completed')
|
||||||
self.assertEquals(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title))
|
self.assertEqual(importer.title, title, 'title for %s should be "%s"' % (source_file_name, title))
|
||||||
for author in author_calls:
|
for author in author_calls:
|
||||||
self.mocked_parse_author.assert_any_call(author)
|
self.mocked_parse_author.assert_any_call(author)
|
||||||
if song_copyright:
|
if song_copyright:
|
||||||
self.mocked_add_copyright.assert_called_with(song_copyright)
|
self.mocked_add_copyright.assert_called_with(song_copyright)
|
||||||
if ccli_number:
|
if ccli_number:
|
||||||
self.assertEquals(importer.ccli_number, ccli_number, 'ccli_number for %s should be %s' %
|
self.assertEqual(importer.ccli_number, ccli_number,
|
||||||
(source_file_name, ccli_number))
|
'ccli_number for %s should be %s' % (source_file_name, ccli_number))
|
||||||
for verse_text, verse_tag in add_verse_calls:
|
for verse_text, verse_tag in add_verse_calls:
|
||||||
self.mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
self.mocked_add_verse.assert_any_call(verse_text, verse_tag)
|
||||||
if topics:
|
if topics:
|
||||||
self.assertEquals(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics))
|
self.assertEqual(importer.topics, topics, 'topics for %s should be %s' % (source_file_name, topics))
|
||||||
if comments:
|
if comments:
|
||||||
self.assertEquals(importer.comments, comments, 'comments for %s should be "%s"' %
|
self.assertEqual(importer.comments, comments,
|
||||||
(source_file_name, comments))
|
'comments for %s should be "%s"' % (source_file_name, comments))
|
||||||
if song_book_name:
|
if song_book_name:
|
||||||
self.assertEquals(importer.song_book_name, song_book_name, 'song_book_name for %s should be "%s"' %
|
self.assertEqual(importer.song_book_name, song_book_name,
|
||||||
(source_file_name, song_book_name))
|
'song_book_name for %s should be "%s"' % (source_file_name, song_book_name))
|
||||||
if song_number:
|
if song_number:
|
||||||
self.assertEquals(importer.song_number, song_number, 'song_number for %s should be %s' %
|
self.assertEqual(importer.song_number, song_number,
|
||||||
(source_file_name, song_number))
|
'song_number for %s should be %s' % (source_file_name, song_number))
|
||||||
if verse_order_list:
|
if verse_order_list:
|
||||||
self.assertEquals(importer.verse_order_list, [], 'verse_order_list for %s should be %s' %
|
self.assertEqual(importer.verse_order_list, [],
|
||||||
(source_file_name, verse_order_list))
|
'verse_order_list for %s should be %s' % (source_file_name, verse_order_list))
|
||||||
self.mocked_finish.assert_called_with()
|
self.mocked_finish.assert_called_with()
|
||||||
|
|
||||||
def _get_data(self, data, key):
|
def _get_data(self, data, key):
|
||||||
|
|
|
@ -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.")
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue