This commit is contained in:
Andreas Preikschat 2014-05-01 18:58:32 +02:00
commit 2ba7fa4f44
121 changed files with 2140 additions and 614 deletions

View File

@ -2,9 +2,12 @@
*.*~
\#*\#
*.eric4project
*.eric5project
*.ropeproject
*.e4*
.eric4project
.komodotools
*.komodoproject
list
openlp.org 2.0.e4*
documentation/build/html

View File

@ -30,7 +30,6 @@
The :mod:`openlp` module contains all the project produced OpenLP functionality
"""
import openlp.core
import openlp.plugins
from openlp import core, plugins
__all__ = ['core', 'plugins']

View File

@ -38,7 +38,7 @@ import traceback
from PyQt4 import QtCore
log = logging.getLogger(__name__+'.__init__')
log = logging.getLogger(__name__ + '.__init__')
FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
@ -76,6 +76,9 @@ def check_directory_exists(directory, do_not_log=False):
def get_frozen_path(frozen_option, non_frozen_option):
"""
Return a path based on the system status.
:param frozen_option:
:param non_frozen_option:
"""
if hasattr(sys, 'frozen') and sys.frozen == 1:
return frozen_option

View File

@ -39,7 +39,7 @@ from PyQt4 import QtCore, QtGui, Qt
from openlp.core.common import translate
log = logging.getLogger(__name__+'.__init__')
log = logging.getLogger(__name__ + '.__init__')
class ServiceItemContext(object):

View File

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

View File

@ -168,29 +168,29 @@ class MediaManagerItem(QtGui.QWidget, RegistryProperties):
Create buttons for the media item toolbar
"""
toolbar_actions = []
## Import Button ##
# Import Button
if self.has_import_icon:
toolbar_actions.append(['Import', StringContent.Import,
':/general/general_import.png', self.on_import_click])
## Load Button ##
# Load Button
if self.has_file_icon:
toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click])
## New Button ##
# New Button
if self.has_new_icon:
toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click])
## Edit Button ##
# Edit Button
if self.has_edit_icon:
toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click])
## Delete Button ##
# Delete Button
if self.has_delete_icon:
toolbar_actions.append(['Delete', StringContent.Delete,
':/general/general_delete.png', self.on_delete_click])
## Preview ##
# Preview
toolbar_actions.append(['Preview', StringContent.Preview,
':/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])
## Add to service Button ##
# Add to service Button
toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click])
for action in toolbar_actions:
if action[0] == StringContent.Preview:

View File

@ -101,7 +101,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
``add_import_menu_item(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.
``create_settings_tab()``
@ -226,7 +226,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
"""
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.
@ -329,22 +329,24 @@ class Plugin(QtCore.QObject, RegistryProperties):
def set_plugin_ui_text_strings(self, tooltips):
"""
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'])
## Import Action ##
# Import Action
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'])
## Edit Action ##
# Edit Action
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'])
## Preview Action ##
# Preview Action
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'])
## Add to Service Action ##
# Add to Service Action
self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service'])
def __set_name_text_string(self, name, title, tooltip):

View File

@ -161,7 +161,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
"""
for plugin in self.plugins:
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):
"""

View File

@ -248,6 +248,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
elif item.is_capable(ItemCapabilities.CanSoftBreak):
pages = []
if '[---]' in text:
# Remove two or more option slide breaks next to each other (causing infinite loop).
while '\n[---]\n[---]\n' in text:
text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
while True:
slides = text.split('\n[---]\n', 2)
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
@ -392,7 +395,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
off when displayed.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``u' '`` or ``u'<br>``.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
"""
formatted = []
previous_html = ''
@ -416,7 +419,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
processed word by word. This is sometimes need for **bible** verses.
:param lines: The text to be fitted on the slide split into lines.
:param line_end: The text added after each line. Either ``u' '`` or ``u'<br>``. This is needed for **bibles**.
:param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
"""
formatted = []
previous_html = ''
@ -453,7 +456,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
(u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong><span style="-webkit-text-fill-color:red">')
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
@ -500,8 +503,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
The text contains html.
:param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
The elements can contain formatting tags.
:param separator: The separator for the elements. For lines this is ``u'<br>'`` and for words this is ``u' '``.
:param line_end: The text added after each "element line". Either ``u' '`` or ``u'<br>``. This is needed for
:param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
:param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
bibles.
"""
smallest_index = 0

View File

@ -387,7 +387,7 @@ class ServiceItem(RegistryProperties):
self.will_auto_start = header.get('will_auto_start', False)
self.processor = header.get('processor', None)
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):
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
self.processor = self.title

View File

@ -173,7 +173,7 @@ def create_button(parent, name, **kwargs):
kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
else:
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)
else:
button = QtGui.QPushButton(parent)

View File

@ -511,7 +511,7 @@ class AdvancedTab(SettingsTab):
"""
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'), '',
file_filters)
if filename:

View File

@ -95,15 +95,15 @@ class Ui_ExceptionDialog(object):
Translate the widgets on the fly.
"""
exception_dialog.setWindowTitle(translate('OpenLP.ExceptionDialog', 'Error Occurred'))
self.description_explanation.setText(translate('OpenLP.ExceptionDialog',
'Please enter a description of what you were doing to cause this error '
'\n(Minimum 20 characters)'))
self.message_label.setText(translate('OpenLP.ExceptionDialog', 'Oops! '
'OpenLP hit a problem, and couldn\'t recover. The text in the box '
'below contains information that might be helpful to the OpenLP '
'developers, so please e-mail it to bugs@openlp.org, along with a '
'detailed description of what you were doing when the problem '
'occurred.'))
self.description_explanation.setText(
translate('OpenLP.ExceptionDialog', 'Please enter a description of what you were doing to cause this error '
'\n(Minimum 20 characters)'))
self.message_label.setText(
translate('OpenLP.ExceptionDialog', 'Oops! OpenLP hit a problem, and couldn\'t recover. The text in the '
'box below contains information that might be helpful to the OpenLP '
'developers, so please e-mail it to bugs@openlp.org, along with a '
'detailed description of what you were doing when the problem '
'occurred.'))
self.send_report_button.setText(translate('OpenLP.ExceptionDialog', 'Send E-Mail'))
self.save_report_button.setText(translate('OpenLP.ExceptionDialog', 'Save to File'))
self.attach_tile_button.setText(translate('OpenLP.ExceptionDialog', 'Attach File'))

View File

@ -228,7 +228,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties):
"""
files = QtGui.QFileDialog.getOpenFileName(self, translate('ImagePlugin.ExceptionDialog', 'Select Attachment'),
Settings().value(self.settings_section + '/last directory'),
'%s (*.*) (*)' % UiStrings().AllFiles)
'%s (*)' % UiStrings().AllFiles)
log.info('New files(s) %s', str(files))
if files:
self.file_attachment = str(files)

View File

@ -67,7 +67,7 @@ class ThemeScreenshotThread(QtCore.QThread):
title = config.get('theme_%s' % theme, 'title')
filename = config.get('theme_%s' % theme, 'filename')
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))
item = QtGui.QListWidgetItem(title, self.parent().themes_list_widget)
item.setData(QtCore.Qt.UserRole, filename)
@ -96,6 +96,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
if self.web_access:
files = self.web_access.read()
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.was_download_cancelled = False
self.theme_screenshot_thread = None
@ -110,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
Run the wizard.
"""
self.setDefaults()
self.set_defaults()
return QtGui.QWizard.exec_(self)
def setDefaults(self):
def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@ -341,7 +345,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
item = self.songs_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
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
# Loop through the Bibles list and increase for each selected item
iterator = QtGui.QTreeWidgetItemIterator(self.bibles_tree_widget)
@ -350,7 +354,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
item = iterator.value()
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
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
iterator += 1
# 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)
if item.checkState() == QtCore.Qt.Checked:
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
if self.max_progress:
# 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.previous_size = 0
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
bibles_iterator = QtGui.QTreeWidgetItemIterator(
self.bibles_tree_widget)
@ -445,7 +449,7 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
bible = item.data(0, QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading % bible, 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
# Download themes
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)
self._increment_progress_bar(self.downloading % theme, 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
if self.display_combo_box.currentIndex() != -1:
Settings().setValue('core/monitor', self.display_combo_box.currentIndex())

View File

@ -211,9 +211,9 @@ class Ui_FirstTimeWizard(object):
first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard'))
self.title_label.setText('<span style="font-size:14pt; font-weight:600;">%s</span>' %
translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard'))
self.information_label.setText(translate('OpenLP.FirstTimeWizard',
'This wizard will help you to configure OpenLP for initial use. '
'Click the next button below to start.'))
self.information_label.setText(
translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. '
'Click the next button below to start.'))
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.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs'))

View File

@ -63,7 +63,6 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont
self.services = FormattingTagController()
self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected)
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.tag_table_widget.currentCellChanged.connect(self.on_current_cell_changed)
self.button_box.rejected.connect(self.close)
@ -202,5 +201,4 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont
if errors:
QtGui.QMessageBox.warning(self, translate('OpenLP.FormattingTagForm', 'Validation Error'), errors,
QtGui.QMessageBox.Ok)
#self.tag_table_widget.selectRow(pre_row - 1)
self.tag_table_widget.resizeRowsToContents()

View File

@ -168,8 +168,10 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
"""
if enabled:
self.setAutoFillBackground(False)
self.setStyleSheet("QGraphicsView {background: transparent; border: 0px;}")
else:
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, False)
self.setStyleSheet("QGraphicsView {}")
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, enabled)
self.repaint()
@ -350,7 +352,6 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
self.hide_display(self.hide_mode)
# Only continue if the visibility wasn't changed during method call.
elif was_visible == self.isVisible():
# Single screen active
if self.screens.display_count == 1:
# Only make visible if setting enabled.

View File

@ -56,29 +56,27 @@ from openlp.core.ui.firsttimeform import FirstTimeForm
log = logging.getLogger(__name__)
MEDIA_MANAGER_STYLE = """
QToolBox {
QToolBox {
padding-bottom: 2px;
}
QToolBox::tab {
}
QToolBox::tab {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 palette(button), stop: 0.5 palette(button),
stop: 1.0 palette(mid));
border: 1px groove palette(mid);
border-radius: 5px;
}
QToolBox::tab:selected {
stop: 0 palette(button), stop: 1.0 palette(mid));
border: 1px solid palette(mid);
border-radius: 3px;
}
QToolBox::tab:selected {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 palette(light), stop: 0.5 palette(midlight),
stop: 1.0 palette(dark));
border: 1px groove palette(dark);
stop: 0 palette(light), stop: 1.0 palette(button));
border: 1px solid palette(mid);
font-weight: bold;
}
}
"""
PROGRESSBAR_STYLE = """
QProgressBar{
height: 10px;
}
QProgressBar{
height: 10px;
}
"""
@ -369,7 +367,7 @@ class Ui_MainWindow(object):
self.settings_menu.setTitle(translate('OpenLP.MainWindow', '&Settings'))
self.settings_language_menu.setTitle(translate('OpenLP.MainWindow', '&Language'))
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.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
@ -396,12 +394,12 @@ class Ui_MainWindow(object):
self.settings_shortcuts_item.setText(translate('OpenLP.MainWindow', 'Configure &Shortcuts...'))
self.formatting_tag_item.setText(translate('OpenLP.MainWindow', 'Configure &Formatting Tags...'))
self.settings_configure_item.setText(translate('OpenLP.MainWindow', '&Configure OpenLP...'))
self.settings_export_item.setStatusTip(translate('OpenLP.MainWindow',
'Export OpenLP settings to a specified *.config file'))
self.settings_export_item.setStatusTip(
translate('OpenLP.MainWindow', 'Export OpenLP settings to a specified *.config file'))
self.settings_export_item.setText(translate('OpenLP.MainWindow', 'Settings'))
self.settings_import_item.setStatusTip(translate('OpenLP.MainWindow',
'Import OpenLP settings from a specified *.config file previously '
'exported on this or another machine'))
self.settings_import_item.setStatusTip(
translate('OpenLP.MainWindow', 'Import OpenLP settings from a specified *.config file previously '
'exported on this or another machine'))
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.setToolTip(translate('OpenLP.MainWindow', 'Toggle Media Manager'))
@ -862,7 +860,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
section = 'general'
section_key = section + "/" + key
# Make sure it's a valid section for us.
if not section in setting_sections:
if section not in setting_sections:
continue
# We have a good file, import it.
for section_key in import_keys:
@ -1336,7 +1334,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
if self.copy_data:
log.info('Copying data to new path')
try:
self.showStatusMessage(
self.show_status_message(
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
'- Please wait for copy to finish').replace('%s', self.new_data_path))
dir_util.copy_tree(old_data_path, self.new_data_path)
@ -1366,8 +1364,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
args = []
for a in self.arguments:
args.extend([a])
for arg in args:
filename = arg
for filename in args:
if not isinstance(filename, str):
filename = str(filename, sys.getfilesystemencoding())
if filename.endswith(('.osz', '.oszl')):

View File

@ -35,7 +35,7 @@ from openlp.core.common import Settings
from PyQt4 import QtCore
log = logging.getLogger(__name__+'.__init__')
log = logging.getLogger(__name__ + '.__init__')
class MediaState(object):
@ -90,7 +90,7 @@ def get_media_players():
overridden_player = 'auto'
else:
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

View File

@ -137,7 +137,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
for player in list(self.media_players.values()):
if player.is_active:
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)
suffix_list.append(item[2:])
self.video_extensions_list = []
@ -184,8 +184,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
return False
saved_players, overridden_player = get_media_players()
invalid_media_players = \
[mediaPlayer for mediaPlayer in saved_players if not mediaPlayer in self.media_players or
not self.media_players[mediaPlayer].check_available()]
[media_player for media_player in saved_players if media_player not in self.media_players or
not self.media_players[media_player].check_available()]
if invalid_media_players:
for invalidPlayer in invalid_media_players:
saved_players.remove(invalidPlayer)
@ -506,7 +506,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
else:
self.media_volume(controller, controller.media_info.volume)
if status:
display.frame.evaluateJavaScript('show_blank("desktop");')
if not controller.media_info.is_background:
display.frame.evaluateJavaScript('show_blank("desktop");')
self.current_media_players[controller.controller_type].set_visible(display, True)
# Flash needs to be played and will not AutoPlay
if controller.media_info.is_flash:
@ -517,7 +518,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
controller.mediabar.actions['playbackPause'].setVisible(True)
controller.mediabar.actions['playbackStop'].setVisible(True)
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()
# Start Timer for ui updates
if not self.timer.isActive():

View File

@ -174,34 +174,11 @@ FLASH_HTML = """
<div id="flash" class="size" style="visibility:hidden"></div>
"""
VIDEO_EXT = [
'*.3gp',
'*.3gpp',
'*.3g2',
'*.3gpp2',
'*.aac',
'*.flv',
'*.f4a',
'*.f4b',
'*.f4p',
'*.f4v',
'*.mov',
'*.m4a',
'*.m4b',
'*.m4p',
'*.m4v',
'*.mkv',
'*.mp4',
'*.ogv',
'*.webm',
'*.mpg', '*.wmv', '*.mpeg', '*.avi',
'*.swf'
]
VIDEO_EXT = ['*.3gp', '*.3gpp', '*.3g2', '*.3gpp2', '*.aac', '*.flv', '*.f4a', '*.f4b', '*.f4p', '*.f4v', '*.mov',
'*.m4a', '*.m4b', '*.m4p', '*.m4v', '*.mkv', '*.mp4', '*.ogv', '*.webm', '*.mpg', '*.wmv', '*.mpeg',
'*.avi', '*.swf']
AUDIO_EXT = [
'*.mp3',
'*.ogg'
]
AUDIO_EXT = ['*.mp3', '*.ogg']
class WebkitPlayer(MediaPlayer):
@ -411,10 +388,9 @@ class WebkitPlayer(MediaPlayer):
"""
Return some information about this player
"""
return(translate('Media.player', 'Webkit is a media player which runs '
'inside a web browser. This player allows text over video to be '
'rendered.') +
'<br/> <strong>' + translate('Media.player', 'Audio') +
'</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' +
translate('Media.player', 'Video') + '</strong><br/>' +
str(VIDEO_EXT) + '<br/>')
part1 = translate('Media.player', 'Webkit is a media player which runs inside a web browser. This player '
'allows text over video to be rendered.')
part2 = translate('Media.player', 'Audio')
part3 = translate('Media.player', 'Video')
return part1 + '<br/> <strong>' + part2 + '</strong><br/>' + str(AUDIO_EXT) + '<br/><strong>' + part3 + \
'</strong><br/>' + str(VIDEO_EXT) + '<br/>'

View File

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

View File

@ -235,7 +235,8 @@ class Ui_ServiceManager(object):
self.edit_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', '&Edit Item'),
icon=':/general/general_edit.png', triggers=self.remote_edit)
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'),
icon=':/general/general_edit.png',
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
"""
for suffix in suffix_list:
if not suffix in self.suffixes:
if suffix not in self.suffixes:
self.suffixes.append(suffix)
def on_new_service_clicked(self, field=None):
@ -631,7 +632,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
for item in self.service_items:
self.main_window.increment_progress_bar()
service_item = item['service_item'].get_service_repr(self._save_lite)
#TODO: check for file item on save.
# TODO: check for file item on save.
service.append({'serviceitem': service_item})
self.main_window.increment_progress_bar()
service_content = json.dumps(service)
@ -756,8 +757,9 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
items = json.load(file_to)
else:
critical_error_message_box(message=translate('OpenLP.ServiceManager',
'The service file you are trying to open is in an old format.\n '
'Please save it using OpenLP 2.0.2 or greater.'))
'The service file you are trying to open is in an old '
'format.\n Please save it using OpenLP 2.0.2 or '
'greater.'))
return
file_to.close()
self.new_file()
@ -1487,9 +1489,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
if new_item:
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.
:param field: Not used, but PyQt needs this.
"""
item = self.find_service_item()[0]
if not self.service_items[item]['service_item'].is_capable(ItemCapabilities.CanEditTitle):

View File

@ -150,5 +150,5 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog, RegistryProperties):
:param function: The function to be called
"""
if not function in self.processes:
if function not in self.processes:
self.processes.append(function)

View File

@ -244,10 +244,10 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
self.primary_push_button.setChecked(False)
self.alternate_push_button.setChecked(False)
else:
if action.defaultShortcuts:
primary_label_text = action.defaultShortcuts[0].toString()
if len(action.defaultShortcuts) == 2:
alternate_label_text = action.defaultShortcuts[1].toString()
if action.default_shortcuts:
primary_label_text = action.default_shortcuts[0].toString()
if len(action.default_shortcuts) == 2:
alternate_label_text = action.default_shortcuts[1].toString()
shortcuts = self._action_shortcuts(action)
# We do not want to loose pending changes, that is why we have to keep the text when, this function has not
# been triggered by a signal.
@ -292,7 +292,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
self._adjust_button(self.alternate_push_button, False, text='')
for category in self.action_list.categories:
for action in category.actions:
self.changed_actions[action] = action.defaultShortcuts
self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
def on_default_radio_button_clicked(self, toggled):
@ -306,7 +306,7 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
if action is None:
return
temp_shortcuts = self._action_shortcuts(action)
self.changed_actions[action] = action.defaultShortcuts
self.changed_actions[action] = action.default_shortcuts
self.refresh_shortcut_list()
primary_button_text = ''
alternate_button_text = ''
@ -357,8 +357,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
return
shortcuts = self._action_shortcuts(action)
new_shortcuts = []
if action.defaultShortcuts:
new_shortcuts.append(action.defaultShortcuts[0])
if action.default_shortcuts:
new_shortcuts.append(action.default_shortcuts[0])
# We have to check if the primary default shortcut is available. But we only have to check, if the action
# has a default primary shortcut (an "empty" shortcut is always valid and if the action does not have a
# default primary shortcut, then the alternative shortcut (not the default one) will become primary
@ -383,8 +383,8 @@ class ShortcutListForm(QtGui.QDialog, Ui_ShortcutListDialog, RegistryProperties)
new_shortcuts = []
if shortcuts:
new_shortcuts.append(shortcuts[0])
if len(action.defaultShortcuts) == 2:
new_shortcuts.append(action.defaultShortcuts[1])
if len(action.default_shortcuts) == 2:
new_shortcuts.append(action.default_shortcuts[1])
if len(new_shortcuts) == 2:
if not self._validiate_shortcut(action, new_shortcuts[1]):
return

View File

@ -495,14 +495,14 @@ class SlideController(DisplayController, RegistryProperties):
self.on_theme_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.
"""
self.keypress_queue.append(ServiceItemAction.Previous)
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.
"""

View File

@ -90,7 +90,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
self.footer_font_combo_box.activated.connect(self.update_theme)
self.footer_size_spin_box.valueChanged.connect(self.update_theme)
def setDefaults(self):
def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@ -261,7 +261,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
log.debug('Editing theme %s' % self.theme.theme_name)
self.temp_background_filename = ''
self.update_theme_allowed = False
self.setDefaults()
self.set_defaults()
self.update_theme_allowed = True
self.theme_name_label.setVisible(not edit)
self.theme_name_edit.setVisible(not edit)
@ -432,7 +432,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties):
Background Image button pushed.
"""
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'), '',
images_filter)
if filename:

View File

@ -44,7 +44,6 @@ class Ui_ThemeLayoutDialog(object):
Set up the UI
"""
themeLayoutDialog.setObjectName('themeLayoutDialogDialog')
#themeLayoutDialog.resize(300, 200)
self.preview_layout = QtGui.QVBoxLayout(themeLayoutDialog)
self.preview_layout.setObjectName('preview_layout')
self.preview_area = QtGui.QWidget(themeLayoutDialog)

View File

@ -114,17 +114,19 @@ class ThemesTab(SettingsTab):
self.global_group_box.setTitle(translate('OpenLP.ThemesTab', 'Global Theme'))
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_label.setText(translate('OpenLP.ThemesTab', 'Use the theme from each song '
'in the database. If a song doesn\'t have a theme associated with '
'it, then use the service\'s theme. If the service doesn\'t have '
'a theme, then use the global theme.'))
self.song_level_label.setText(
translate('OpenLP.ThemesTab', 'Use the theme from each song in the database. If a song doesn\'t have a '
'theme associated with it, then use the service\'s theme. If the service '
'doesn\'t have a theme, then use the global theme.'))
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, '
'overriding any of the individual songs\' themes. If the '
'service doesn\'t have a theme, then use the global theme.'))
self.service_level_label.setText(
translate('OpenLP.ThemesTab', 'Use the theme from the service, overriding any of the individual '
'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_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding '
'any themes associated with either the service or the songs.'))
self.global_level_label.setText(translate('OpenLP.ThemesTab', 'Use the global theme, overriding any themes '
'associated with either the service or the '
'songs.'))
def load(self):
"""

View File

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

View File

@ -56,7 +56,7 @@ if sys.platform != 'win32' and sys.platform != 'darwin':
from openlp.core.common import translate
log = logging.getLogger(__name__+'.__init__')
log = logging.getLogger(__name__ + '.__init__')
APPLICATION_VERSION = {}
IMAGES_FILTER = None

View File

@ -65,20 +65,14 @@ class CategoryActionList(object):
self.index = 0
self.actions = []
def __getitem__(self, key):
"""
Implement the __getitem__() method to make this class a dictionary type
"""
for weight, action in self.actions:
if action.text() == key:
return action
raise KeyError('Action "%s" does not exist.' % key)
def __contains__(self, item):
def __contains__(self, key):
"""
Implement the __contains__() method to make this class a dictionary type
"""
return item in self
for weight, action in self.actions:
if action == key:
return True
return False
def __len__(self):
"""
@ -103,23 +97,14 @@ class CategoryActionList(object):
self.index += 1
return self.actions[self.index - 1][1]
def has_key(self, key):
"""
Implement the has_key() method to make this class a dictionary type
"""
for weight, action in self.actions:
if action.text() == key:
return True
return False
def append(self, name):
def append(self, action):
"""
Append an action
"""
weight = 0
if self.actions:
weight = self.actions[-1][0] + 1
self.add(name, weight)
self.add(action, weight)
def add(self, action, weight=0):
"""
@ -128,14 +113,15 @@ class CategoryActionList(object):
self.actions.append((weight, action))
self.actions.sort(key=lambda act: act[0])
def remove(self, remove_action):
def remove(self, action):
"""
Remove an action
"""
for action in self.actions:
if action[1] == remove_action:
self.actions.remove(action)
for item in self.actions:
if item[1] == action:
self.actions.remove(item)
return
raise ValueError('Action "%s" does not exist.' % action)
class CategoryList(object):
@ -184,9 +170,9 @@ class CategoryList(object):
self.index += 1
return self.categories[self.index - 1]
def has_key(self, key):
def __contains__(self, key):
"""
Implement the has_key() method to make this class like a dictionary
Implement the __contains__() method to make this class like a dictionary
"""
for category in self.categories:
if category.name == key:
@ -200,10 +186,7 @@ class CategoryList(object):
weight = 0
if self.categories:
weight = self.categories[-1].weight + 1
if actions:
self.add(name, weight, actions)
else:
self.add(name, weight)
self.add(name, weight, actions)
def add(self, name, weight=0, actions=None):
"""
@ -226,6 +209,8 @@ class CategoryList(object):
for category in self.categories:
if category.name == name:
self.categories.remove(category)
return
raise ValueError('Category "%s" does not exist.' % name)
class ActionList(object):
@ -270,7 +255,7 @@ class ActionList(object):
settings = Settings()
settings.beginGroup('shortcuts')
# Get the default shortcut from the config.
action.defaultShortcuts = settings.get_default_value(action.objectName())
action.default_shortcuts = settings.get_default_value(action.objectName())
if weight is None:
self.categories[category].actions.append(action)
else:

View File

@ -207,12 +207,12 @@ class AlertsPlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('AlertsPlugin', 'Alert', 'name singular'),
'plural': translate('AlertsPlugin', 'Alerts', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('AlertsPlugin', 'Alerts', 'container title')
}

View File

@ -88,8 +88,6 @@ class BiblePlugin(Plugin):
self.import_bible_item.setVisible(True)
action_list = ActionList.get_instance()
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
self.export_bible_item.setVisible(False)
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.remove_action(self.import_bible_item, UiStrings().Import)
self.import_bible_item.setVisible(False)
#action_list.remove_action(self.export_bible_item, UiStrings().Export)
self.export_bible_item.setVisible(False)
def app_startup(self):
@ -115,19 +112,27 @@ class BiblePlugin(Plugin):
if self.manager.old_bible_databases:
if QtGui.QMessageBox.information(
self.main_window, translate('OpenLP', 'Information'),
translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your existing Bibles.\n'
'Should OpenLP upgrade now?'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \
translate('OpenLP', 'Bible format has changed.\nYou have to upgrade your '
'existing Bibles.\nShould OpenLP upgrade now?'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)) == \
QtGui.QMessageBox.Yes:
self.on_tools_upgrade_Item_triggered()
def add_import_menu_item(self, import_menu):
"""
:param import_menu:
"""
self.import_bible_item = create_action(import_menu, 'importBibleItem',
text=translate('BiblesPlugin', '&Bible'), visible=False,
triggers=self.on_bible_import_click)
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',
text=translate('BiblesPlugin', '&Bible'), visible=False)
export_menu.addAction(self.export_bible_item)
@ -190,12 +195,12 @@ class BiblePlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('BiblesPlugin', 'Bible', 'name singular'),
'plural': translate('BiblesPlugin', 'Bibles', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('BiblesPlugin', 'Bibles', 'container title')
}

View File

@ -465,7 +465,7 @@ class BibleImportForm(OpenLPWizard):
self.license_details_page.registerField('license_copyright', self.copyright_edit)
self.license_details_page.registerField('license_permissions', self.permissions_edit)
def setDefaults(self):
def set_defaults(self):
"""
Set default values for the wizard pages.
"""

View File

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

View File

@ -32,9 +32,11 @@ import logging
import os
import re
import sqlite3
import time
from PyQt4 import QtCore
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.exc import UnmappedClassError
@ -235,7 +237,12 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
text=verse_text
)
self.session.add(verse)
self.session.commit()
try:
self.session.commit()
except OperationalError:
# Wait 10ms and try again (lp#1154467)
time.sleep(0.01)
self.session.commit()
def create_verse(self, book_id, chapter, verse, text):
"""
@ -649,10 +656,10 @@ class BiblesResourcesDB(QtCore.QObject, Manager):
'chapter, verse_count FROM chapters WHERE book_reference_id = ?', (book_ref_id,))
try:
return {
'id': chapters[chapter-1][0],
'book_reference_id': chapters[chapter-1][1],
'chapter': chapters[chapter-1][2],
'verse_count': chapters[chapter-1][3]
'id': chapters[chapter - 1][0],
'book_reference_id': chapters[chapter - 1][1],
'chapter': chapters[chapter - 1][2],
'verse_count': chapters[chapter - 1][3]
}
except (IndexError, TypeError):
return None
@ -846,13 +853,13 @@ class AlternativeBookNamesDB(QtCore.QObject, Manager):
file_path = os.path.join(
AppLocation.get_directory(AppLocation.DataDir), 'bibles', 'alternative_book_names.sqlite')
if not os.path.exists(file_path):
#create new DB, create table alternative_book_names
# create new DB, create table alternative_book_names
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)
AlternativeBookNamesDB.conn.execute(
'CREATE TABLE alternative_book_names(id INTEGER NOT NULL, '
'book_reference_id INTEGER, language_id INTEGER, name VARCHAR(50), PRIMARY KEY (id))')
else:
#use existing DB
# use existing DB
AlternativeBookNamesDB.conn = sqlite3.connect(file_path)
AlternativeBookNamesDB.cursor = AlternativeBookNamesDB.conn.cursor()
return AlternativeBookNamesDB.cursor

View File

@ -552,10 +552,10 @@ class HTTPBible(BibleDB, RegistryProperties):
self.application.set_busy_cursor()
search_results = self.get_chapter(book, reference[1])
if search_results and search_results.has_verse_list():
## We have found a book of the bible lets check to see
## if it was there. By reusing the returned book name
## we get a correct book. For example it is possible
## to request ac and get Acts back.
# We have found a book of the bible lets check to see
# if it was there. By reusing the returned book name
# we get a correct book. For example it is possible
# to request ac and get Acts back.
book_name = search_results.book
self.application.process_events()
# Check to see if book/chapter exists.

View File

@ -480,6 +480,10 @@ class BibleMediaItem(MediaManagerItem):
self.reload_bibles()
def on_delete_click(self):
"""
When the delete button is pressed
"""
bible = None
if self.quickTab.isVisible():
bible = self.quickVersionComboBox.currentText()
elif self.advancedTab.isVisible():
@ -488,8 +492,9 @@ class BibleMediaItem(MediaManagerItem):
if QtGui.QMessageBox.question(
self, UiStrings().ConfirmDelete,
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,
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No),
'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.Yes) == QtGui.QMessageBox.No:
return
self.plugin.manager.delete_bible(bible)
@ -752,7 +757,7 @@ class BibleMediaItem(MediaManagerItem):
log.exception('The second_search_results does not have as many verses as the search_results.')
break
bible_text = '%s %d%s%d (%s, %s)' % (book, verse.chapter, verse_separator, verse.verse, version,
second_version)
second_version)
else:
bible_text = '%s %d%s%d (%s)' % (book, verse.chapter, verse_separator, verse.verse, version)
bible_verse = QtGui.QListWidgetItem(bible_text)

View File

@ -73,13 +73,13 @@ class OpenSongBible(BibleDB):
log.debug('Starting OpenSong import from "%s"' % self.filename)
if not isinstance(self.filename, str):
self.filename = str(self.filename, 'utf8')
file = None
import_file = None
success = True
try:
# 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.
file = open(self.filename, 'r')
opensong = objectify.parse(file)
import_file = open(self.filename, 'rb')
opensong = objectify.parse(import_file)
bible = opensong.getroot()
language_id = self.get_language(bible_name)
if not language_id:
@ -93,7 +93,7 @@ class OpenSongBible(BibleDB):
log.error('Importing books from "%s" failed' % self.filename)
return False
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
for chapter in book.c:
if self.stop_import_flag:
@ -122,8 +122,8 @@ class OpenSongBible(BibleDB):
verse_number += 1
self.create_verse(db_book.id, chapter_number, verse_number, self.get_text(verse))
self.wizard.increment_progress_bar(
translate('BiblesPlugin.Opensong', 'Importing %s %s...',
'Importing <book name> <chapter>...')) % (db_book.name, chapter_number)
translate('BiblesPlugin.Opensong', 'Importing %(bookname)s %(chapter)s...' %
{'bookname': db_book.name, 'chapter': chapter_number}))
self.session.commit()
self.application.process_events()
except etree.XMLSyntaxError as inst:
@ -137,8 +137,8 @@ class OpenSongBible(BibleDB):
log.exception('Loading Bible from OpenSong file failed')
success = False
finally:
if file:
file.close()
if import_file:
import_file.close()
if self.stop_import_flag:
return False
else:

View File

@ -94,5 +94,5 @@ class VerseReferenceList(object):
result = result + ', ' + version['permission']
result = result.rstrip()
if result.endswith(','):
return result[:len(result)-1]
return result[:len(result) - 1]
return result

View File

@ -43,7 +43,7 @@ log = logging.getLogger(__name__)
__default_settings__ = {
'custom/db type': 'sqlite',
'custom/last search type': CustomSearch.Titles,
'custom/last search type': CustomSearch.Titles,
'custom/display footer': True,
'custom/add custom from service': True
}
@ -97,12 +97,12 @@ class CustomPlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('CustomPlugin', 'Custom Slide', 'name singular'),
'plural': translate('CustomPlugin', 'Custom Slides', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('CustomPlugin', 'Custom Slides', 'container title')
}

View File

@ -1,4 +1,3 @@
#lint:disable
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4

View File

@ -51,7 +51,7 @@ from lxml import etree, objectify
log = logging.getLogger(__name__)
#TODO: These classes need to be refactored into a single class.
# TODO: These classes need to be refactored into a single class.
class CustomXMLBuilder(object):
"""
This class builds the XML used to describe songs.

View File

@ -95,12 +95,12 @@ class ImagePlugin(Plugin):
"""
Called to define all translatable texts of the plugin.
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('ImagePlugin', 'Image', 'name singular'),
'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')}
# Middle Header Bar
tooltips = {

View File

@ -75,7 +75,7 @@ class ImageMediaItem(MediaManagerItem):
def retranslateUi(self):
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
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.setToolTip(UiStrings().AddGroup)
self.replace_action.setText(UiStrings().ReplaceBG)

View File

@ -95,7 +95,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
self.reset_action.setToolTip(UiStrings().ResetLiveBG)
self.automatic = UiStrings().Automatic
self.display_type_label.setText(translate('MediaPlugin.MediaItem', 'Use Player:'))
#self.rebuild_players()
def required_icons(self):
"""
@ -141,7 +140,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
if index == 0:
set_media_players(player)
else:
set_media_players(player, player[index-1])
set_media_players(player, player[index - 1])
def on_reset_click(self):
"""

View File

@ -73,12 +73,12 @@ class MediaPlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('MediaPlugin', 'Media', 'name singular'),
'plural': translate('MediaPlugin', 'Media', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('MediaPlugin', 'Media', 'container title')
}

View File

@ -377,8 +377,6 @@ class ImpressDocument(PresentationDocument):
Stop the presentation, remove from screen.
"""
log.debug('stop presentation OpenOffice')
# deactivate should hide the screen according to docs, but doesn't
#self.control.deactivate()
self.presentation.end()
self.control = None

View File

@ -74,7 +74,7 @@ class PdfController(PresentationController):
runlog = ''
log.debug('testing program_path: %s', program_path)
try:
runlog = check_output([program_path, '--help'], stderr=STDOUT)
runlog = check_output([program_path, '--help'], stderr=STDOUT)
except CalledProcessError as e:
runlog = e.output
except Exception:
@ -183,7 +183,7 @@ class PdfDocument(PresentationDocument):
self.image_files = []
self.num_pages = -1
def gs_get_resolution(self, size):
def gs_get_resolution(self, size):
"""
Only used when using ghostscript
Ghostscript can't scale automatically while keeping aspect like mupdf, so we need
@ -204,19 +204,19 @@ class PdfDocument(PresentationDocument):
log.debug(' '.join(e.cmd))
log.debug(e.output)
# Extract the pdf resolution from output, the format is " Size: x: <width>, y: <height>"
width = 0
height = 0
width = 0.0
height = 0.0
for line in runlog.splitlines():
try:
width = int(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))
width = float(re.search('.*Size: x: (\d+\.?\d*), y: \d+.*', line.decode()).group(1))
height = float(re.search('.*Size: x: \d+\.?\d*, y: (\d+\.?\d*).*', line.decode()).group(1))
break
except AttributeError:
pass
continue
# Calculate the ratio from pdf to screen
if width > 0 and height > 0:
width_ratio = size.right() / float(width)
height_ratio = size.bottom() / float(height)
width_ratio = size.right() / width
height_ratio = size.bottom() / height
# return the resolution that should be used. 72 is default.
if width_ratio > height_ratio:
return int(height_ratio * 72)
@ -236,7 +236,7 @@ class PdfDocument(PresentationDocument):
if os.path.isfile(os.path.join(self.get_temp_folder(), 'mainslide001.png')):
created_files = sorted(os.listdir(self.get_temp_folder()))
for fn in created_files:
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
if os.path.isfile(os.path.join(self.get_temp_folder(), fn)):
self.image_files.append(os.path.join(self.get_temp_folder(), fn))
self.num_pages = len(self.image_files)
return True

View File

@ -247,7 +247,7 @@ class PowerpointDocument(PresentationDocument):
Starts a presentation from the beginning.
"""
log.debug('start_presentation')
#SlideShowWindow measures its size/position by points, not pixels
# SlideShowWindow measures its size/position by points, not pixels
try:
dpi = win32ui.GetActiveWindow().GetDC().GetDeviceCaps(88)
except win32ui.error:

View File

@ -122,8 +122,10 @@ class PresentationDocument(object):
a file, e.g. thumbnails
"""
try:
shutil.rmtree(self.get_thumbnail_folder())
shutil.rmtree(self.get_temp_folder())
if os.path.exists(self.get_thumbnail_folder()):
shutil.rmtree(self.get_thumbnail_folder())
if os.path.exists(self.get_temp_folder()):
shutil.rmtree(self.get_temp_folder())
except OSError:
log.exception('Failed to delete presentation controller files')

View File

@ -175,10 +175,10 @@ class PresentationTab(SettingsTab):
if pdf_program == '':
enable_pdf_program = 0
if pdf_program != Settings().value(self.settings_section + '/pdf_program'):
Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
Settings().setValue(self.settings_section + '/pdf_program', pdf_program)
changed = True
if enable_pdf_program != Settings().value(self.settings_section + '/enable_pdf_program'):
Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)
Settings().setValue(self.settings_section + '/enable_pdf_program', enable_pdf_program)
changed = True
if changed:
self.settings_form.register_post_process('mediaitem_suffix_reset')

View File

@ -156,12 +156,12 @@ class PresentationPlugin(Plugin):
"""
Called to define all translatable texts of the plugin.
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('PresentationPlugin', 'Presentation', 'name singular'),
'plural': translate('PresentationPlugin', 'Presentations', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('PresentationPlugin', 'Presentations', 'container title')
}

View File

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

View File

@ -40,7 +40,7 @@ import time
from PyQt4 import QtCore
from openlp.core.common import AppLocation, Settings
from openlp.core.common import AppLocation, Settings, RegistryProperties
from openlp.plugins.remotes.lib import HttpRouter
@ -94,13 +94,18 @@ class HttpThread(QtCore.QThread):
"""
self.http_server.start_server()
def stop(self):
log.debug("stop called")
self.http_server.stop = True
class OpenLPServer():
class OpenLPServer(RegistryProperties):
def __init__(self):
"""
Initialise the http server, and start the server of the correct type http / https
"""
log.debug('Initialise httpserver')
super(OpenLPServer, self).__init__()
log.debug('Initialise OpenLP')
self.settings_section = 'remotes'
self.http_thread = HttpThread(self)
self.http_thread.start()
@ -110,32 +115,49 @@ class OpenLPServer():
Start the correct server and save the handler
"""
address = Settings().value(self.settings_section + '/ip address')
if Settings().value(self.settings_section + '/https enabled'):
self.address = address
self.is_secure = Settings().value(self.settings_section + '/https enabled')
self.needs_authentication = Settings().value(self.settings_section + '/authentication enabled')
if self.is_secure:
port = Settings().value(self.settings_section + '/https port')
self.httpd = HTTPSServer((address, port), CustomHandler)
log.debug('Started ssl httpd...')
self.port = port
self.start_server_instance(address, port, HTTPSServer)
else:
port = Settings().value(self.settings_section + '/port')
loop = 1
while loop < 3:
try:
self.httpd = ThreadingHTTPServer((address, port), CustomHandler)
except OSError:
loop += 1
time.sleep(0.1)
except:
log.error('Failed to start server ')
log.debug('Started non ssl httpd...')
self.port = port
self.start_server_instance(address, port, ThreadingHTTPServer)
if hasattr(self, 'httpd') and self.httpd:
self.httpd.serve_forever()
else:
log.debug('Failed to start server')
def start_server_instance(self, address, port, server_class):
"""
Start the server
:param address: The server address
:param port: The run port
:param server_class: the class to start
"""
loop = 1
while loop < 4:
try:
self.httpd = server_class((address, port), CustomHandler)
log.debug("Server started for class %s %s %d" % (server_class, address, port))
except OSError:
log.debug("failed to start http server thread state %d %s" %
(loop, self.http_thread.isRunning()))
loop += 1
time.sleep(0.1)
except:
log.error('Failed to start server ')
def stop_server(self):
"""
Stop the server
"""
self.http_thread.exit(0)
if self.http_thread.isRunning():
self.http_thread.stop()
self.httpd = None
log.debug('Stopped the server.')

View File

@ -32,7 +32,7 @@ import os.path
from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.common import AppLocation, Settings, translate
from openlp.core.lib import SettingsTab
from openlp.core.lib import SettingsTab, build_icon
ZERO_URL = '0.0.0.0'
@ -234,6 +234,7 @@ class RemoteTab(SettingsTab):
"""
Load the configuration and update the server configuration if necessary
"""
self.is_secure = Settings().value(self.settings_section + '/https enabled')
self.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
self.https_port_spin_box.setValue(Settings().value(self.settings_section + '/https port'))
self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
@ -263,9 +264,7 @@ class RemoteTab(SettingsTab):
Settings().value(self.settings_section + '/port') != self.port_spin_box.value() or \
Settings().value(self.settings_section + '/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settings_section + '/https enabled') != \
self.https_settings_group_box.isChecked() or \
Settings().value(self.settings_section + '/authentication enabled') != \
self.user_login_group_box.isChecked():
self.https_settings_group_box.isChecked():
self.settings_form.register_post_process('remotes_config_updated')
Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
Settings().setValue(self.settings_section + '/https port', self.https_port_spin_box.value())
@ -275,6 +274,7 @@ class RemoteTab(SettingsTab):
Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settings_section + '/user id', self.user_id.text())
Settings().setValue(self.settings_section + '/password', self.password.text())
self.generate_icon()
def on_twelve_hour_check_box_changed(self, check_state):
"""
@ -290,3 +290,25 @@ class RemoteTab(SettingsTab):
Invert the HTTP group box based on Https group settings
"""
self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
def generate_icon(self):
"""
Generate icon for main window
"""
self.remote_server_icon.hide()
icon = QtGui.QImage(':/remote/network_server.png')
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
if self.is_secure:
overlay = QtGui.QImage(':/remote/network_ssl.png')
overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
painter.drawImage(0, 0, overlay)
painter.end()
if Settings().value(self.settings_section + '/authentication enabled'):
overlay = QtGui.QImage(':/remote/network_auth.png')
overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
painter = QtGui.QPainter(icon)
painter.drawImage(20, 0, overlay)
painter.end()
self.remote_server_icon.setPixmap(QtGui.QPixmap.fromImage(icon))
self.remote_server_icon.show()

View File

@ -28,7 +28,8 @@
###############################################################################
import logging
import time
from PyQt4 import QtGui
from openlp.core.lib import Plugin, StringContent, translate, build_icon
from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
@ -67,6 +68,21 @@ class RemotesPlugin(Plugin):
log.debug('initialise')
super(RemotesPlugin, self).initialise()
self.server = OpenLPServer()
if not hasattr(self, 'remote_server_icon'):
self.remote_server_icon = QtGui.QLabel(self.main_window.status_bar)
size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
self.remote_server_icon.setSizePolicy(size_policy)
self.remote_server_icon.setFrameShadow(QtGui.QFrame.Plain)
self.remote_server_icon.setLineWidth(1)
self.remote_server_icon.setScaledContents(True)
self.remote_server_icon.setFixedSize(20, 20)
self.remote_server_icon.setObjectName('remote_server_icon')
self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
self.settings_tab.remote_server_icon = self.remote_server_icon
self.settings_tab.generate_icon()
def finalise(self):
"""
@ -92,21 +108,23 @@ class RemotesPlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('RemotePlugin', 'Remote', 'name singular'),
'plural': translate('RemotePlugin', 'Remotes', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('RemotePlugin', 'Remote', 'container title')
}
def config_update(self):
"""
Called when Config is changed to restart the server on new address or port
Called when Config is changed to requests a restart with the server on new address or port
"""
log.debug('remote config changed')
self.finalise()
time.sleep(0.5)
self.initialise()
QtGui.QMessageBox.information(self.main_window,
translate('RemotePlugin', 'Server Config Change'),
translate('RemotePlugin', 'Server configuration changes will require a restart '
'to take effect.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))

View File

@ -31,6 +31,7 @@ The duplicate song removal logic for OpenLP.
"""
import logging
import multiprocessing
import os
from PyQt4 import QtCore, QtGui
@ -45,6 +46,18 @@ from openlp.plugins.songs.lib.songcompare import songs_probably_equal
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):
"""
This is the Duplicate Song Removal Wizard. It provides functionality to search for and remove duplicate songs
@ -114,7 +127,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.review_layout.addWidget(self.review_scroll_area)
self.review_page_id = self.addPage(self.review_page)
# Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the
#review page.
# review page.
self.dummy_page = QtGui.QWizardPage()
self.dummy_page_id = self.addPage(self.dummy_page)
@ -167,24 +180,31 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
max_progress_count = max_songs * (max_songs - 1) // 2
self.duplicate_search_progress_bar.setMaximum(max_progress_count)
songs = self.plugin.manager.get_all_objects(Song)
for outer_song_counter in range(max_songs - 1):
for inner_song_counter in range(outer_song_counter + 1, max_songs):
if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]):
duplicate_added = self.add_duplicates_to_song_list(
songs[outer_song_counter], songs[inner_song_counter])
if duplicate_added:
self.found_duplicates_edit.appendPlainText(
songs[outer_song_counter].title + " = " + songs[inner_song_counter].title)
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
# The call to process_events() will keep the GUI responsive.
self.application.process_events()
if self.break_search:
return
# Create a worker/process pool to check the songs.
process_number = max(1, multiprocessing.cpu_count() - 1)
pool = multiprocessing.Pool(process_number)
result = pool.imap_unordered(songs_probably_equal, song_generator(songs), 30)
# Do not accept any further tasks. Also this closes the processes if all tasks are done.
pool.close()
# While the processes are still working, start to look at the results.
for song_tuple in result:
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
# The call to process_events() will keep the GUI responsive.
self.application.process_events()
if self.break_search:
pool.terminate()
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)
if self.review_total_count == 0:
self.notify_no_duplicates()
else:
if self.duplicate_song_list:
self.button(QtGui.QWizard.NextButton).show()
else:
self.notify_no_duplicates()
finally:
self.application.set_normal_cursor()
elif page_id == self.review_page_id:
@ -217,12 +237,12 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
duplicate_added = False
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.
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_found = True
duplicate_added = True
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_found = True
duplicate_added = True
@ -244,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True
self.plugin.media_item.on_search_text_button_clicked()
def setDefaults(self):
def set_defaults(self):
"""
Set default form values for the song import wizard.
"""

View File

@ -118,13 +118,18 @@ class Ui_EditSongDialog(object):
self.authors_group_box.setObjectName('authors_group_box')
self.authors_layout = QtGui.QVBoxLayout(self.authors_group_box)
self.authors_layout.setObjectName('authors_layout')
self.author_add_layout = QtGui.QHBoxLayout()
self.author_add_layout = QtGui.QVBoxLayout()
self.author_add_layout.setObjectName('author_add_layout')
self.author_type_layout = QtGui.QHBoxLayout()
self.author_type_layout.setObjectName('author_type_layout')
self.authors_combo_box = create_combo_box(self.authors_group_box, 'authors_combo_box')
self.author_add_layout.addWidget(self.authors_combo_box)
self.author_types_combo_box = create_combo_box(self.authors_group_box, 'author_types_combo_box', editable=False)
self.author_type_layout.addWidget(self.author_types_combo_box)
self.author_add_button = QtGui.QPushButton(self.authors_group_box)
self.author_add_button.setObjectName('author_add_button')
self.author_add_layout.addWidget(self.author_add_button)
self.author_type_layout.addWidget(self.author_add_button)
self.author_add_layout.addLayout(self.author_type_layout)
self.authors_layout.addLayout(self.author_add_layout)
self.authors_list_view = QtGui.QListWidget(self.authors_group_box)
self.authors_list_view.setAlternatingRowColors(True)
@ -330,7 +335,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
def create_combo_box(parent, name):
def create_combo_box(parent, name, editable=True):
"""
Utility method to generate a standard combo box for this dialog.
@ -340,7 +345,7 @@ def create_combo_box(parent, name):
combo_box = QtGui.QComboBox(parent)
combo_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
combo_box.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
combo_box.setEditable(True)
combo_box.setEditable(editable)
combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert)
combo_box.setObjectName(name)
return combo_box

View File

@ -42,7 +42,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, UiStri
from openlp.core.lib import FileDialog, PluginStatus, MediaType, create_separated_list
from openlp.core.lib.ui import set_case_insensitive_completer, critical_error_message_box, find_and_set_in_combo_box
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Book, Song, Author, Topic, MediaFile
from openlp.plugins.songs.lib.db import Book, Song, Author, AuthorSong, AuthorType, Topic, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML
from openlp.plugins.songs.forms.editsongdialog import Ui_EditSongDialog
@ -122,12 +122,12 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
combo.setItemData(row, obj.id)
set_case_insensitive_completer(cache, combo)
def _add_author_to_list(self, author):
def _add_author_to_list(self, author, author_type):
"""
Add an author to the author list.
"""
author_item = QtGui.QListWidgetItem(str(author.display_name))
author_item.setData(QtCore.Qt.UserRole, author.id)
author_item = QtGui.QListWidgetItem(author.get_display_name(author_type))
author_item.setData(QtCore.Qt.UserRole, (author.id, author_type))
self.authors_list_view.addItem(author_item)
def _extract_verse_order(self, verse_order):
@ -217,8 +217,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
if self.authors_list_view.count() == 0:
self.song_tab_widget.setCurrentIndex(1)
self.authors_list_view.setFocus()
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'You need to have an author for this song.'))
critical_error_message_box(message=translate('SongsPlugin.EditSongForm',
'You need to have an author for this song.'))
return False
if self.verse_order_edit.text():
result = self._validate_verse_list(self.verse_order_edit.text(), self.verse_list_widget.rowCount())
@ -302,6 +302,15 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.authors.append(author.display_name)
set_case_insensitive_completer(self.authors, self.authors_combo_box)
# Types
self.author_types_combo_box.clear()
self.author_types_combo_box.addItem('')
# Don't iterate over the dictionary to give them this specific order
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Words], AuthorType.Words)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Music], AuthorType.Music)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.WordsAndMusic], AuthorType.WordsAndMusic)
self.author_types_combo_box.addItem(AuthorType.Types[AuthorType.Translation], AuthorType.Translation)
def load_topics(self):
"""
Load the topics into the combobox.
@ -454,10 +463,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.tag_rows()
# clear the results
self.authors_list_view.clear()
for author in self.song.authors:
author_name = QtGui.QListWidgetItem(str(author.display_name))
author_name.setData(QtCore.Qt.UserRole, author.id)
self.authors_list_view.addItem(author_name)
for author_song in self.song.authors_songs:
self._add_author_to_list(author_song.author, author_song.author_type)
# clear the results
self.topics_list_view.clear()
for topic in self.song.topics:
@ -496,6 +503,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
"""
item = int(self.authors_combo_box.currentIndex())
text = self.authors_combo_box.currentText().strip(' \r\n\t')
author_type = self.author_types_combo_box.itemData(self.author_types_combo_box.currentIndex())
# This if statement is for OS X, which doesn't seem to work well with
# the QCompleter auto-completion class. See bug #812628.
if text in self.authors:
@ -513,7 +521,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
author = Author.populate(first_name=text.rsplit(' ', 1)[0], last_name=text.rsplit(' ', 1)[1],
display_name=text)
self.manager.save_object(author)
self._add_author_to_list(author)
self._add_author_to_list(author, author_type)
self.load_authors()
self.authors_combo_box.setCurrentIndex(0)
else:
@ -521,11 +529,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0:
item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id)
if self.authors_list_view.findItems(str(author.display_name), QtCore.Qt.MatchExactly):
if self.authors_list_view.findItems(author.get_display_name(author_type), QtCore.Qt.MatchExactly):
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else:
self._add_author_to_list(author)
self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0)
else:
QtGui.QMessageBox.warning(
@ -675,14 +683,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
separator = parts.find(':')
if separator >= 0:
verse_name = parts[0:separator].strip()
verse_num = parts[separator+1:].strip()
verse_num = parts[separator + 1:].strip()
else:
verse_name = parts
verse_num = '1'
verse_index = VerseType.from_loose_input(verse_name)
verse_tag = VerseType.tags[verse_index]
# Later we need to handle v1a as well.
#regex = re.compile(r'(\d+\w.)')
regex = re.compile(r'\D*(\d+)\D*')
match = regex.match(verse_num)
if match:
@ -906,13 +913,13 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
else:
self.song.theme_name = None
self._process_lyrics()
self.song.authors = []
self.song.authors_songs = []
for row in range(self.authors_list_view.count()):
item = self.authors_list_view.item(row)
author_id = (item.data(QtCore.Qt.UserRole))
author = self.manager.get_object(Author, author_id)
if author is not None:
self.song.authors.append(author)
author_song = AuthorSong()
author_song.author_id = item.data(QtCore.Qt.UserRole)[0]
author_song.author_type = item.data(QtCore.Qt.UserRole)[1]
self.song.authors_songs.append(author_song)
self.song.topics = []
for row in range(self.topics_list_view.count()):
item = self.topics_list_view.item(row)

View File

@ -75,7 +75,7 @@ class EditVerseForm(QtGui.QDialog, Ui_EditVerseDialog):
text = self.verse_text_edit.toPlainText()
position = self.verse_text_edit.textCursor().position()
insert_string = '[---]'
if position and text[position-1] != '\n':
if position and text[position - 1] != '\n':
insert_string = '\n' + insert_string
if position == len(text) or text[position] != '\n':
insert_string += '\n'

View File

@ -66,8 +66,10 @@ class Ui_MediaFilesDialog(object):
def retranslateUi(self, media_files_dialog):
"""
Translate the UI on the fly.
:param media_files_dialog:
"""
media_files_dialog.setWindowTitle(translate('SongsPlugin.MediaFilesForm', 'Select Media File(s)'))
self.select_label.setText(translate('SongsPlugin.MediaFilesForm',
'Select one or more audio files from the list below, and click OK to import them '
'into this song.'))
self.select_label.setText(translate('SongsPlugin.MediaFilesForm', 'Select one or more audio files from the '
'list below, and click OK to import them '
'into this song.'))

View File

@ -152,9 +152,9 @@ class SongExportForm(OpenLPWizard):
self.setWindowTitle(translate('SongsPlugin.ExportWizardForm', 'Song Export Wizard'))
self.title_label.setText(WizardStrings.HeaderStyle %
translate('OpenLP.Ui', 'Welcome to the Song Export Wizard'))
self.information_label.setText(translate('SongsPlugin.ExportWizardForm', 'This wizard will help to'
' export your songs to the open and free <strong>OpenLyrics </strong> worship '
'song format.'))
self.information_label.setText(
translate('SongsPlugin.ExportWizardForm', 'This wizard will help to export your songs to the open and free '
'<strong>OpenLyrics </strong> worship song format.'))
self.available_songs_page.setTitle(translate('SongsPlugin.ExportWizardForm', 'Select Songs'))
self.available_songs_page.setSubTitle(translate('SongsPlugin.ExportWizardForm',
'Check the songs you want to export.'))

View File

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

View File

@ -390,7 +390,7 @@ def clean_song(manager, song):
verses = SongXML().get_verses(song.lyrics)
song.search_lyrics = ' '.join([clean_string(verse[1]) for verse in verses])
# The song does not have any author, add one.
if not song.authors:
if not song.authors and not song.authors_songs: # Need to check both relations
name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name)
if author is None:

View File

@ -270,13 +270,13 @@ class CCLIFileImport(SongImport):
verse_text = ''
verse_start = False
else:
#line_number=0, song title
# line_number=0, song title
if line_number == 0:
self.title = clean_line
line_number += 1
#line_number=1, verses
# line_number=1, verses
elif line_number == 1:
#line_number=1, ccli number, first line after verses
# line_number=1, ccli number, first line after verses
if clean_line.startswith('CCLI'):
line_number += 1
ccli_parts = clean_line.split(' ')
@ -319,21 +319,21 @@ class CCLIFileImport(SongImport):
# last part. Add l so as to keep the CRLF
verse_text = verse_text + line
else:
#line_number=2, copyright
# line_number=2, copyright
if line_number == 2:
line_number += 1
if clean_line.startswith('©'):
self.copyright = clean_line
else:
song_author = clean_line
#n=3, authors
# n=3, authors
elif line_number == 3:
line_number += 1
if song_author:
self.copyright = clean_line
else:
song_author = clean_line
#line_number=4, comments lines before last line
# line_number=4, comments lines before last line
elif line_number == 4 and not clean_line.startswith('CCL'):
self.comments += clean_line
# split on known separators

View File

@ -35,19 +35,52 @@ import re
from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.sql.expression import func
from sqlalchemy.sql.expression import func, text
from openlp.core.lib.db import BaseModel, init_db
from openlp.core.utils import get_natural_key
from openlp.core.lib import translate
class Author(BaseModel):
"""
Author model
"""
def get_display_name(self, author_type=None):
if author_type:
return "%s (%s)" % (self.display_name, AuthorType.Types[author_type])
return self.display_name
class AuthorSong(BaseModel):
"""
Relationship between Authors and Songs (many to many).
Need to define this relationship table explicit to get access to the
Association Object (author_type).
http://docs.sqlalchemy.org/en/latest/orm/relationships.html#association-object
"""
pass
class AuthorType(object):
"""
Enumeration for Author types.
They are defined by OpenLyrics: http://openlyrics.info/dataformat.html#authors
The 'words+music' type is not an official type, but is provided for convenience.
"""
Words = 'words'
Music = 'music'
WordsAndMusic = 'words+music'
Translation = 'translation'
Types = {
Words: translate('OpenLP.Ui', 'Words'),
Music: translate('OpenLP.Ui', 'Music'),
WordsAndMusic: translate('OpenLP.Ui', 'Words and Music'),
Translation: translate('OpenLP.Ui', 'Translation')
}
class Book(BaseModel):
"""
Book model
@ -67,6 +100,7 @@ class Song(BaseModel):
"""
Song model
"""
def __init__(self):
self.sort_key = []
@ -120,6 +154,7 @@ def init_schema(url):
* author_id
* song_id
* author_type
**media_files Table**
* id
@ -230,7 +265,8 @@ def init_schema(url):
authors_songs_table = Table(
'authors_songs', metadata,
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True)
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('author_type', types.String(), primary_key=True, nullable=False, server_default=text('""'))
)
# Definition of the "songs_topics" table
@ -241,10 +277,15 @@ def init_schema(url):
)
mapper(Author, authors_table)
mapper(AuthorSong, authors_songs_table, properties={
'author': relation(Author)
})
mapper(Book, song_books_table)
mapper(MediaFile, media_files_table)
mapper(Song, songs_table, properties={
'authors': relation(Author, backref='songs', secondary=authors_songs_table, lazy=False),
# Use the authors_songs relation when you need access to the 'author_type' attribute.
'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"),
'authors': relation(Author, secondary=authors_songs_table),
'book': relation(Book, backref='songs'),
'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight),
'topics': relation(Topic, backref='songs', secondary=songs_topics_table)

View File

@ -34,13 +34,13 @@ EasyWorship song databases into the current installation database.
import os
import struct
import re
import zlib
from openlp.core.lib import translate
from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib import retrieve_windows_encoding, strip_rtf
from .songimport import SongImport
RTF_STRIPPING_REGEX = re.compile(r'\{\\tx[^}]*\}')
# regex: at least two newlines, can have spaces between them
SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*')
NUMBER_REGEX = re.compile(r'[0-9]+')
@ -77,9 +77,121 @@ class EasyWorshipSongImport(SongImport):
def do_import(self):
"""
Import the songs
Determines the type of file to import and calls the appropiate method
"""
if self.import_source.lower().endswith('ews'):
self.import_ews()
else:
self.import_db()
:return:
def import_ews(self):
"""
Import the songs from service file
The full spec of the ews files can be found here:
https://github.com/meinders/lithium-ews/blob/master/docs/ews%20file%20format.md
or here: http://wiki.openlp.org/Development:EasyWorship_EWS_Format
"""
# Open ews file if it exists
if not os.path.isfile(self.import_source):
log.debug('Given ews file does not exists.')
return
# Make sure there is room for at least a header and one entry
if os.path.getsize(self.import_source) < 892:
log.debug('Given ews file is to small to contain valid data.')
return
# Take a stab at how text is encoded
self.encoding = 'cp1252'
self.encoding = retrieve_windows_encoding(self.encoding)
if not self.encoding:
log.debug('No encoding set.')
return
self.ews_file = open(self.import_source, 'rb')
# EWS header, version '1.6'/' 3'/' 5':
# Offset Field Data type Length Details
# --------------------------------------------------------------------------------------------------
# 0 Filetype string 38 Specifies the file type and version.
# "EasyWorship Schedule File Version 1.6" or
# "EasyWorship Schedule File Version 3" or
# "EasyWorship Schedule File Version 5"
# 40/48/56 Entry count int32le 4 Number of items in the schedule
# 44/52/60 Entry length int16le 2 Length of schedule entries: 0x0718 = 1816
# Get file version
type, = struct.unpack('<38s', self.ews_file.read(38))
version = type.decode()[-3:]
# Set fileposition based on filetype/version
file_pos = 0
if version == ' 5':
file_pos = 56
elif version == ' 3':
file_pos = 48
elif version == '1.6':
file_pos = 40
else:
log.debug('Given ews file is of unknown version.')
return
entry_count = self.get_i32(file_pos)
entry_length = self.get_i16(file_pos+4)
file_pos += 6
self.import_wizard.progress_bar.setMaximum(entry_count)
# Loop over songs
for i in range(entry_count):
# Load EWS entry metadata:
# Offset Field Data type Length Details
# ------------------------------------------------------------------------------------------------
# 0 Title cstring 50
# 307 Author cstring 50
# 358 Copyright cstring 100
# 459 Administrator cstring 50
# 800 Content pointer int32le 4 Position of the content for this entry.
# 820 Content type int32le 4 0x01 = Song, 0x02 = Scripture, 0x03 = Presentation,
# 0x04 = Video, 0x05 = Live video, 0x07 = Image,
# 0x08 = Audio, 0x09 = Web
# 1410 Song number cstring 10
self.set_defaults()
self.title = self.get_string(file_pos + 0, 50)
authors = self.get_string(file_pos + 307, 50)
copyright = self.get_string(file_pos + 358, 100)
admin = self.get_string(file_pos + 459, 50)
cont_ptr = self.get_i32(file_pos + 800)
cont_type = self.get_i32(file_pos + 820)
self.ccli_number = self.get_string(file_pos + 1410, 10)
# Only handle content type 1 (songs)
if cont_type != 1:
file_pos += entry_length
continue
# Load song content
# Offset Field Data type Length Details
# ------------------------------------------------------------------------------------------------
# 0 Length int32le 4 Length (L) of content, including the compressed content
# and the following fields (14 bytes total).
# 4 Content string L-14 Content compressed with deflate.
# Checksum int32be 4 Alder-32 checksum.
# (unknown) 4 0x51 0x4b 0x03 0x04
# Content length int32le 4 Length of content after decompression
content_length = self.get_i32(cont_ptr)
deflated_content = self.get_bytes(cont_ptr + 4, content_length - 10)
deflated_length = self.get_i32(cont_ptr + 4 + content_length - 6)
inflated_content = zlib.decompress(deflated_content, 15, deflated_length)
if copyright:
self.copyright = copyright
if admin:
if copyright:
self.copyright += ', '
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
'Administered by %s') % admin
# Set the SongImport object members.
self.set_song_import_object(authors, inflated_content)
if self.stop_import_flag:
break
if not self.finish():
self.log_error(self.import_source)
# Set file_pos for next entry
file_pos += entry_length
self.ews_file.close()
def import_db(self):
"""
Import the songs from the database
"""
# Open the DB and MB files if they exist
import_source_mb = self.import_source.replace('.DB', '.MB')
@ -176,7 +288,6 @@ class EasyWorshipSongImport(SongImport):
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
# Set the SongImport object members.
if copy:
self.copyright = copy.decode()
if admin:
@ -187,55 +298,11 @@ class EasyWorshipSongImport(SongImport):
if ccli:
self.ccli_number = ccli.decode()
if authors:
# Split up the authors
author_list = authors.split(b'/')
if len(author_list) < 2:
author_list = authors.split(b';')
if len(author_list) < 2:
author_list = authors.split(b',')
for author_name in author_list:
self.add_author(author_name.decode().strip())
if words:
# Format the lyrics
result = strip_rtf(words.decode(), self.encoding)
if result is None:
return
words, self.encoding = result
verse_type = VerseType.tags[VerseType.Verse]
for verse in SLIDE_BREAK_REGEX.split(words):
verse = verse.strip()
if not verse:
continue
verse_split = verse.split('\n', 1)
first_line_is_tag = False
# EW tags: verse, chorus, pre-chorus, bridge, tag,
# intro, ending, slide
for tag in VerseType.tags + ['tag', 'slide']:
tag = tag.lower()
ew_tag = verse_split[0].strip().lower()
if ew_tag.startswith(tag):
verse_type = tag[0]
if tag == 'tag' or tag == 'slide':
verse_type = VerseType.tags[VerseType.Other]
first_line_is_tag = True
number_found = False
# check if tag is followed by number and/or note
if len(ew_tag) > len(tag):
match = NUMBER_REGEX.search(ew_tag)
if match:
number = match.group()
verse_type += number
number_found = True
match = NOTE_REGEX.search(ew_tag)
if match:
self.comments += ew_tag + '\n'
if not number_found:
verse_type += '1'
break
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
if len(self.comments) > 5:
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
'\n[above are Song Tags with notes imported from EasyWorship]'))
authors = authors.decode()
else:
authors = ''
# Set the SongImport object members.
self.set_song_import_object(authors, words)
if self.stop_import_flag:
break
if not self.finish():
@ -243,12 +310,69 @@ class EasyWorshipSongImport(SongImport):
db_file.close()
self.memo_file.close()
def set_song_import_object(self, authors, words):
"""
Set the SongImport object members.
:param authors: String with authons
:param words: Bytes with rtf-encoding
"""
if authors:
# Split up the authors
author_list = authors.split('/')
if len(author_list) < 2:
author_list = authors.split(';')
if len(author_list) < 2:
author_list = authors.split(',')
for author_name in author_list:
self.add_author(author_name.strip())
if words:
# Format the lyrics
result = strip_rtf(words.decode(), self.encoding)
if result is None:
return
words, self.encoding = result
verse_type = VerseType.tags[VerseType.Verse]
for verse in SLIDE_BREAK_REGEX.split(words):
verse = verse.strip()
if not verse:
continue
verse_split = verse.split('\n', 1)
first_line_is_tag = False
# EW tags: verse, chorus, pre-chorus, bridge, tag,
# intro, ending, slide
for tag in VerseType.tags + ['tag', 'slide']:
tag = tag.lower()
ew_tag = verse_split[0].strip().lower()
if ew_tag.startswith(tag):
verse_type = tag[0]
if tag == 'tag' or tag == 'slide':
verse_type = VerseType.tags[VerseType.Other]
first_line_is_tag = True
number_found = False
# check if tag is followed by number and/or note
if len(ew_tag) > len(tag):
match = NUMBER_REGEX.search(ew_tag)
if match:
number = match.group()
verse_type += number
number_found = True
match = NOTE_REGEX.search(ew_tag)
if match:
self.comments += ew_tag + '\n'
if not number_found:
verse_type += '1'
break
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
if len(self.comments) > 5:
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',
'\n[above are Song Tags with notes imported from EasyWorship]'))
def find_field(self, field_name):
"""
Find a field in the descriptions
:param field_name: field to find
:return:
"""
return [i for i, x in enumerate(self.field_descriptions) if x.name == field_name][0]
@ -285,7 +409,7 @@ class EasyWorshipSongImport(SongImport):
Extract the field
:param field_desc_index: Field index value
:return:
:return: The field value
"""
field = self.fields[field_desc_index]
field_desc = self.field_descriptions[field_desc_index]
@ -305,7 +429,7 @@ class EasyWorshipSongImport(SongImport):
elif field_desc.field_type == FieldType.Logical:
return field ^ 0x80 == 1
elif field_desc.field_type == FieldType.Memo or field_desc.field_type == FieldType.Blob:
block_start, blob_size = struct.unpack_from('<II', field, len(field)-10)
block_start, blob_size = struct.unpack_from('<II', field, len(field) - 10)
sub_block = block_start & 0xff
block_start &= ~0xff
self.memo_file.seek(block_start)
@ -323,3 +447,52 @@ class EasyWorshipSongImport(SongImport):
return self.memo_file.read(blob_size)
else:
return 0
def get_bytes(self, pos, length):
"""
Get bytes from ews_file
:param pos: Position to read from
:param length: Bytes to read
:return: Bytes read
"""
self.ews_file.seek(pos)
return self.ews_file.read(length)
def get_string(self, pos, length):
"""
Get string from ews_file
:param pos: Position to read from
:param length: Characters to read
:return: String read
"""
bytes = self.get_bytes(pos, length)
mask = '<' + str(length) + 's'
byte_str, = struct.unpack(mask, bytes)
return byte_str.decode('unicode-escape').replace('\0', '').strip()
def get_i16(self, pos):
"""
Get short int from ews_file
:param pos: Position to read from
:return: Short integer read
"""
bytes = self.get_bytes(pos, 2)
mask = '<h'
number, = struct.unpack(mask, bytes)
return number
def get_i32(self, pos):
"""
Get long int from ews_file
:param pos: Position to read from
:return: Long integer read
"""
bytes = self.get_bytes(pos, 4)
mask = '<i'
number, = struct.unpack(mask, bytes)
return number

View File

@ -153,19 +153,20 @@ class SongFormat(object):
CCLI = 3
DreamBeam = 4
EasySlides = 5
EasyWorship = 6
FoilPresenter = 7
MediaShout = 8
OpenSong = 9
PowerSong = 10
SongBeamer = 11
SongPro = 12
SongShowPlus = 13
SongsOfFellowship = 14
SundayPlus = 15
WordsOfWorship = 16
WorshipCenterPro = 17
ZionWorx = 18
EasyWorshipDB = 6
EasyWorshipService = 7
FoilPresenter = 8
MediaShout = 9
OpenSong = 10
PowerSong = 11
SongBeamer = 12
SongPro = 13
SongShowPlus = 14
SongsOfFellowship = 15
SundayPlus = 16
WordsOfWorship = 17
WorshipCenterPro = 18
ZionWorx = 19
# Set optional attribute defaults
__defaults__ = {
@ -224,13 +225,20 @@ class SongFormat(object):
'selectMode': SongFormatSelect.SingleFile,
'filter': '%s (*.xml)' % translate('SongsPlugin.ImportWizardForm', 'EasySlides XML File')
},
EasyWorship: {
EasyWorshipDB: {
'class': EasyWorshipSongImport,
'name': 'EasyWorship',
'name': 'EasyWorship Song Database',
'prefix': 'ew',
'selectMode': SongFormatSelect.SingleFile,
'filter': '%s (*.db)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Song Database')
},
EasyWorshipService: {
'class': EasyWorshipSongImport,
'name': 'EasyWorship Service File',
'prefix': 'ew',
'selectMode': SongFormatSelect.SingleFile,
'filter': '%s (*.ews)' % translate('SongsPlugin.ImportWizardForm', 'EasyWorship Service File')
},
FoilPresenter: {
'class': FoilPresenterImport,
'name': 'Foilpresenter',
@ -341,7 +349,8 @@ class SongFormat(object):
SongFormat.CCLI,
SongFormat.DreamBeam,
SongFormat.EasySlides,
SongFormat.EasyWorship,
SongFormat.EasyWorshipDB,
SongFormat.EasyWorshipService,
SongFormat.FoilPresenter,
SongFormat.MediaShout,
SongFormat.OpenSong,

View File

@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@ -96,7 +96,7 @@ class SongMediaItem(MediaManagerItem):
def add_end_header_bar(self):
self.toolbar.addSeparator()
## Song Maintenance Button ##
# Song Maintenance Button
self.maintenance_action = self.toolbar.add_toolbar_action('maintenance_action',
icon=':/songs/song_maintenance.png',
triggers=self.on_song_maintenance_click)
@ -192,7 +192,7 @@ class SongMediaItem(MediaManagerItem):
song_number = False
if not search_results:
search_keywords = search_keywords.rpartition(' ')
search_string = '%' + search_keywords + '%'
search_string = '%' + search_keywords[0] + '%'
search_results = self.plugin.manager.get_all_objects(Book,
Book.name.like(search_string), Book.name.asc())
song_number = re.sub(r'[^0-9]', '', search_keywords[2])
@ -234,8 +234,7 @@ class SongMediaItem(MediaManagerItem):
if song.temporary:
continue
author_list = [author.display_name for author in song.authors]
song_title = str(song.title)
song_detail = '%s (%s)' % (song_title, create_separated_list(author_list))
song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title
song_name = QtGui.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)
@ -266,7 +265,7 @@ class SongMediaItem(MediaManagerItem):
# Do not display temporary songs
if song.temporary:
continue
if song_number and not song_number in song.song_number:
if song_number and song_number not in song.song_number:
continue
song_detail = '%s - %s (%s)' % (book.name, song.song_number, song.title)
song_name = QtGui.QListWidgetItem(song_detail)
@ -464,23 +463,53 @@ class SongMediaItem(MediaManagerItem):
def generate_footer(self, item, song):
"""
Generates the song footer based on a song and adds details to a service item.
author_list is only required for initial song generation.
:param item: The service item to be amended
:param song: The song to be used to generate the footer
:return: List of all authors (only required for initial song generation)
"""
author_list = [str(author.display_name) for author in song.authors]
authors_words = []
authors_music = []
authors_words_music = []
authors_translation = []
authors_none = []
for author_song in song.authors_songs:
if author_song.author_type == AuthorType.Words:
authors_words.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.Music:
authors_music.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.WordsAndMusic:
authors_words_music.append(author_song.author.display_name)
elif author_song.author_type == AuthorType.Translation:
authors_translation.append(author_song.author.display_name)
else:
authors_none.append(author_song.author.display_name)
authors_all = authors_words_music + authors_words + authors_music + authors_translation + authors_none
item.audit = [
song.title, author_list, song.copyright, str(song.ccli_number)
song.title, authors_all, song.copyright, str(song.ccli_number)
]
item.raw_footer = []
item.raw_footer.append(song.title)
item.raw_footer.append(create_separated_list(author_list))
if authors_none:
item.raw_footer.append("%s: %s" % (translate('OpenLP.Ui', 'Written by'),
create_separated_list(authors_none)))
if authors_words_music:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.WordsAndMusic],
create_separated_list(authors_words_music)))
if authors_words:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Words],
create_separated_list(authors_words)))
if authors_music:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Music],
create_separated_list(authors_music)))
if authors_translation:
item.raw_footer.append("%s: %s" % (AuthorType.Types[AuthorType.Translation],
create_separated_list(authors_translation)))
item.raw_footer.append(song.copyright)
if Settings().value('core/ccli number'):
item.raw_footer.append(translate('SongsPlugin.MediaItem',
'CCLI License: ') + Settings().value('core/ccli number'))
return author_list
return authors_all
def service_load(self, item):
"""

View File

@ -53,7 +53,7 @@ class OpenLyricsImport(SongImport):
Initialise the Open Lyrics importer.
"""
log.debug('initialise OpenLyricsImport')
SongImport.__init__(self, manager, **kwargs)
super(OpenLyricsImport, self).__init__(manager, **kwargs)
self.open_lyrics = OpenLyrics(self.manager)
def do_import(self):
@ -69,7 +69,7 @@ class OpenLyricsImport(SongImport):
try:
# Pass a file object, because lxml does not cope with some
# 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()
self.open_lyrics.xml_to_song(xml)
except etree.XMLSyntaxError:

View File

@ -56,11 +56,15 @@ class SongBeamerTypes(object):
'Zwischenspiel': VerseType.tags[VerseType.Bridge],
'Pre-Chorus': VerseType.tags[VerseType.PreChorus],
'Pre-Refrain': VerseType.tags[VerseType.PreChorus],
'Misc': VerseType.tags[VerseType.Other],
'Pre-Bridge': 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],
'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()
if line.startswith('#') and not read_verses:
self.parseTags(line)
elif line.startswith('---'):
elif line.startswith('--'):
# --- and -- allowed for page-breaks (difference in Songbeamer only in printout)
if self.current_verse:
self.replace_html_tags()
self.add_verse(self.current_verse, self.current_verse_type)
@ -282,4 +287,7 @@ class SongBeamerImport(SongImport):
if marks[1].isdigit():
self.current_verse_type += marks[1]
return True
elif marks[0].startswith('$$M='): # this verse-mark cannot be numbered
self.current_verse_type = SongBeamerTypes.MarkTypes['$$M=']
return True
return False

View File

@ -52,13 +52,13 @@ MIN_BLOCK_SIZE = 70
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.
:param song1: The first song to compare.
:param song2: The second song to compare.
:param song_tupel: A tuple of two songs to compare.
"""
song1, song2 = song_tupel
if len(song1.search_lyrics) < len(song2.search_lyrics):
small = song1.search_lyrics
large = song2.search_lyrics
@ -75,18 +75,19 @@ def songs_probably_equal(song1, song2):
for element in diff_no_typos:
if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE:
length_of_equal_blocks += _op_length(element)
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.
# Calculate the length of the largest equal block of the diff set.
length_of_longest_equal_block = 0
for element in diff_no_typos:
if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block:
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:
return True
if length_of_longest_equal_block > len(small) * 2 // 3:
return song1, song2
# Both checks failed. We assume the songs are not equal.
return False
return None
def _op_length(opcode):

View File

@ -40,7 +40,7 @@ class SongStrings(object):
# These strings should need a good reason to be retranslated elsewhere.
Author = translate('OpenLP.Ui', 'Author', 'Singular')
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
AuthorUnknown = 'Author Unknown' # Used to populate the database.
AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
SongBook = translate('OpenLP.Ui', 'Song Book', 'Singular')
SongBooks = translate('OpenLP.Ui', 'Song Books', 'Plural')

View File

@ -32,14 +32,14 @@ backend for the Songs plugin
"""
import logging
from sqlalchemy import Column, types
from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.exc import OperationalError
from sqlalchemy.sql.expression import func, false, null, text
from openlp.core.lib.db import get_upgrade_op
log = logging.getLogger(__name__)
__version__ = 3
__version__ = 4
def upgrade_1(session, metadata):
@ -97,3 +97,25 @@ def upgrade_3(session, metadata):
op.add_column('songs', Column('temporary', types.Boolean(), server_default=false()))
except OperationalError:
log.info('Upgrade 3 has already been run')
def upgrade_4(session, metadata):
"""
Version 4 upgrade.
This upgrade adds a column for author type to the authors_songs table
"""
try:
# Since SQLite doesn't support changing the primary key of a table, we need to recreate the table
# and copy the old values
op = get_upgrade_op(session)
op.create_table('authors_songs_tmp',
Column('author_id', types.Integer(), ForeignKey('authors.id'), primary_key=True),
Column('song_id', types.Integer(), ForeignKey('songs.id'), primary_key=True),
Column('author_type', types.String(), primary_key=True,
nullable=False, server_default=text('""')))
op.execute('INSERT INTO authors_songs_tmp SELECT author_id, song_id, "" FROM authors_songs')
op.drop_table('authors_songs')
op.rename_table('authors_songs_tmp', 'authors_songs')
except OperationalError:
log.info('Upgrade 4 has already been run')

View File

@ -71,7 +71,7 @@ from lxml import etree, objectify
from openlp.core.common import translate
from openlp.core.lib import FormattingTags
from openlp.plugins.songs.lib import VerseType, clean_song
from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
from openlp.plugins.songs.lib.db import Author, AuthorSong, AuthorType, Book, Song, Topic
from openlp.core.utils import get_application_version
log = logging.getLogger(__name__)
@ -166,7 +166,7 @@ class OpenLyrics(object):
supported by the :class:`OpenLyrics` class:
``<authors>``
OpenLP does not support the attribute *type* and *lang*.
OpenLP does not support the attribute *lang*.
``<chord>``
This property is not supported.
@ -269,10 +269,18 @@ class OpenLyrics(object):
'verseOrder', properties, song.verse_order.lower())
if song.ccli_number:
self._add_text_to_element('ccliNo', properties, song.ccli_number)
if song.authors:
if song.authors_songs:
authors = etree.SubElement(properties, 'authors')
for author in song.authors:
self._add_text_to_element('author', authors, author.display_name)
for author_song in song.authors_songs:
element = self._add_text_to_element('author', authors, author_song.author.display_name)
if author_song.author_type:
# Handle the special case 'words+music': Need to create two separate authors for that
if author_song.author_type == AuthorType.WordsAndMusic:
element.set('type', AuthorType.Words)
element = self._add_text_to_element('author', authors, author_song.author.display_name)
element.set('type', AuthorType.Music)
else:
element.set('type', author_song.author_type)
book = self.manager.get_object_filtered(Book, Book.id == song.song_book_id)
if book is not None:
book = book.name
@ -501,16 +509,20 @@ class OpenLyrics(object):
if hasattr(properties, 'authors'):
for author in properties.authors.author:
display_name = self._text(author)
author_type = author.get('type', '')
if display_name:
authors.append(display_name)
for display_name in authors:
authors.append((display_name, author_type))
for (display_name, author_type) in authors:
author = self.manager.get_object_filtered(Author, Author.display_name == display_name)
if author is None:
# We need to create a new author, as the author does not exist.
author = Author.populate(display_name=display_name,
last_name=display_name.split(' ')[-1],
first_name=' '.join(display_name.split(' ')[:-1]))
song.authors.append(author)
author_song = AuthorSong()
author_song.author = author
author_song.author_type = author_type
song.authors_songs.append(author_song)
def _process_cclinumber(self, properties, song):
"""
@ -675,6 +687,7 @@ class OpenLyrics(object):
sxml = SongXML()
verses = {}
verse_def_list = []
verse_order = self._text(properties.verseOrder).split(' ') if hasattr(properties, 'verseOrder') else []
try:
lyrics = song_xml.lyrics
except AttributeError:
@ -717,13 +730,17 @@ class OpenLyrics(object):
else:
verses[(verse_tag, verse_number, lang, translit, verse_part)] = text
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.
for verse in verse_def_list:
sxml.add_verse_to_lyrics(verse[0], verse[1], verses[verse], verse[2])
song_obj.lyrics = str(sxml.extract_xml(), 'utf-8')
# Process verse order
if hasattr(properties, 'verseOrder'):
song_obj.verse_order = self._text(properties.verseOrder)
song_obj.verse_order = ' '.join(verse_order)
def _process_songbooks(self, properties, song):
"""

View File

@ -132,7 +132,7 @@ class SongsPlugin(Plugin):
)
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.
@ -261,12 +261,12 @@ class SongsPlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('SongsPlugin', 'Song', 'name singular'),
'plural': translate('SongsPlugin', 'Songs', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('SongsPlugin', 'Songs', 'container title')
}

View File

@ -246,12 +246,12 @@ class SongUsagePlugin(Plugin):
"""
Called to define all translatable texts of the plugin
"""
## Name PluginList ##
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('SongUsagePlugin', 'SongUsage', 'name singular'),
'plural': translate('SongUsagePlugin', 'SongUsage', 'name plural')
}
## Name for MediaDockManager, SettingsManager ##
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'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

View File

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

View 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
import re
from requests.exceptions import HTTPError
from subprocess import Popen, PIPE
import sys
@ -49,6 +50,9 @@ from jenkins import Jenkins
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):
@ -59,9 +63,20 @@ class OpenLPJobs(object):
Branch_Functional = 'Branch-02-Functional-Tests'
Branch_Interface = 'Branch-03-Interface-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):
@ -79,14 +94,25 @@ class JenkinsTrigger(object):
"""
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):
"""
Print the status information of the build tirggered.
"""
print("Add this to your merge proposal:")
print("--------------------------------")
print('Add this to your merge proposal:')
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:
self.__print_build_info(job)
@ -107,22 +133,22 @@ class JenkinsTrigger(object):
"""
job = self.jenkins_instance.job(job_name)
while job.info['inQueue']:
# Give other processes the possibility to take over. Like Thread.yield().
time.sleep(0)
time.sleep(1)
build = job.last_build
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']
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():
"""
This returns the name of branch of the wokring directory. For example it returns *lp:~googol/openlp/render*.
This returns the name of branch of the working directory. For example it returns *lp:~googol/openlp/render*.
"""
# Run the bzr command.
bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
@ -139,46 +165,41 @@ def get_repo_name():
for line in output_list:
# Check if it is remote branch.
if 'push branch' in line:
repo_name = line.replace('push branch: bzr+ssh://bazaar.launchpad.net/', 'lp:')
break
match = re.match(REPO_REGEX, line)
if match:
repo_name = 'lp:%s' % match.group(2)
break
elif 'checkout of branch' in line:
repo_name = line.replace('checkout of branch: bzr+ssh://bazaar.launchpad.net/', 'lp:')
break
repo_name = 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
match = re.match(REPO_REGEX, line)
if match:
repo_name = 'lp:%s' % match.group(2)
break
return repo_name.strip('/')
def main():
usage = 'Usage: python %prog TOKEN [options]'
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.')
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.')
#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)
if len(args) == 2:
if not get_repo_name():
print('Not a branch. Have you pushed it to launchpad? Did you cd to the branch?')
return
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)
try:
jenkins_trigger.trigger_build()
except HTTPError as e:
print("Wrong token.")
except HTTPError:
print('Wrong token.')
return
# Open the browser before printing the output.
if options.open_browser:

View File

@ -96,7 +96,7 @@ class CommandStack(object):
return len(self.data)
def __getitem__(self, index):
if not index in self.data:
if index not in self.data:
return None
elif self.data[index].get('arguments'):
return self.data[index]['command'], self.data[index]['arguments']

View File

@ -32,12 +32,13 @@ Functional tests to test the AppLocation class and related methods.
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):
"""
@ -64,3 +65,19 @@ class TestInitFunctions(TestCase):
# THEN: the new string should be converted to python format
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')

View File

@ -52,7 +52,7 @@ class TestRegistryProperties(TestCase, RegistryProperties):
# GIVEN an Empty Registry
# WHEN there is no Application
# 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):
"""
@ -63,4 +63,4 @@ class TestRegistryProperties(TestCase, RegistryProperties):
# WHEN the application is registered
Registry().register('application', application)
# 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')

View File

@ -50,8 +50,8 @@ class TestDB(TestCase):
"""
# GIVEN: Mocked out SQLAlchemy calls and return objects, and an in-memory SQLite database URL
with patch('openlp.core.lib.db.create_engine') as mocked_create_engine, \
patch('openlp.core.lib.db.MetaData') as MockedMetaData, \
patch('openlp.core.lib.db.sessionmaker') as mocked_sessionmaker, \
patch('openlp.core.lib.db.MetaData') as MockedMetaData, \
patch('openlp.core.lib.db.sessionmaker') as mocked_sessionmaker, \
patch('openlp.core.lib.db.scoped_session') as mocked_scoped_session:
mocked_engine = MagicMock()
mocked_metadata = MagicMock()

View File

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

View File

@ -30,12 +30,16 @@
Package to test the openlp.core.ui package.
"""
import os
import time
from threading import Lock
from unittest import TestCase
from PyQt4 import QtGui
from openlp.core.common import Registry
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
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'resources'))
@ -51,6 +55,8 @@ class TestImageManager(TestCase, TestMixin):
self.get_application()
ScreenList.create(self.app.desktop())
self.image_manager = ImageManager()
self.lock = Lock()
self.sleep_time = 0.1
def tearDown(self):
"""
@ -82,3 +88,87 @@ class TestImageManager(TestCase, TestMixin):
with self.assertRaises(KeyError) as context:
self.image_manager.get_image(TEST_PATH, 'church1.jpg')
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 ''

View File

@ -482,7 +482,7 @@ class TestLib(TestCase):
# WHEN: we run the validate_thumb() function
# THEN: we should have called a few functions, and the result should be True
#mocked_os.path.exists.assert_called_with(thumb_path)
# mocked_os.path.exists.assert_called_with(thumb_path)
def validate_thumb_file_exists_and_older_test(self):
"""

View File

@ -212,9 +212,9 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_export_menu()
plugin_manager.hook_export_menu()
# THEN: The add_export_menu_Item() method should not have been called
self.assertEqual(0, mocked_plugin.add_export_menu_Item.call_count,
'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,
'The add_export_menu_item() method should not have been called.')
def hook_export_menu_with_active_plugin_test(self):
"""
@ -229,8 +229,8 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_export_menu()
plugin_manager.hook_export_menu()
# 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)
# 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)
def hook_upgrade_plugin_settings_with_disabled_plugin_test(self):
"""
@ -264,7 +264,7 @@ class TestPluginManager(TestCase):
# WHEN: We run hook_upgrade_plugin_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)
def hook_tools_menu_with_disabled_plugin_test(self):

View File

@ -49,7 +49,7 @@ class TestRenderer(TestCase):
def setUp(self):
"""
Set up the components need for all tests.
Set up the components need for all tests
"""
# Mocked out desktop object
self.desktop = MagicMock()
@ -67,7 +67,7 @@ class TestRenderer(TestCase):
def initial_renderer_test(self):
"""
Test the initial renderer state .
Test the initial renderer state
"""
# GIVEN: A new renderer instance.
renderer = Renderer()
@ -77,7 +77,7 @@ class TestRenderer(TestCase):
def default_screen_layout_test(self):
"""
Test the default layout calculations.
Test the default layout calculations
"""
# GIVEN: A new renderer instance.
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.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')
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)

View File

@ -82,6 +82,38 @@ class TestUi(TestCase):
self.assertEqual(1, len(btnbox.buttons()))
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):
"""
Test creating a combo box for valign selection
@ -98,3 +130,43 @@ class TestUi(TestCase):
self.assertEqual(combo, label.buddy())
for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]:
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())

View File

@ -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')

View File

@ -70,7 +70,7 @@ class TestFormattingTagForm(TestCase):
form.save_button = MagicMock()
# WHEN: on_text_edited is called with an arbitrary value
#form.on_text_edited('text')
# form.on_text_edited('text')
# THEN: setEnabled and setDefault should have been called on save_push_button
#form.save_button.setEnabled.assert_called_with(True)
# form.save_button.setEnabled.assert_called_with(True)

View File

@ -79,3 +79,31 @@ class TestMainDisplay(TestCase):
# THEN: The controller should not 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')

View File

@ -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')

View File

@ -35,9 +35,107 @@ from PyQt4 import QtGui, QtCore
from openlp.core.common import Settings
from openlp.core.utils import ActionList
from openlp.core.utils.actions import CategoryActionList
from tests.functional import MagicMock
from tests.helpers.testmixin import TestMixin
class TestCategoryActionList(TestCase):
def setUp(self):
"""
Create an instance and a few example actions.
"""
self.action1 = MagicMock()
self.action1.text.return_value = 'first'
self.action2 = MagicMock()
self.action2.text.return_value = 'second'
self.list = CategoryActionList()
def tearDown(self):
"""
Clean up
"""
del self.list
def contains_test(self):
"""
Test the __contains__() method
"""
# GIVEN: The list.
# WHEN: Add an action
self.list.append(self.action1)
# THEN: The actions should (not) be in the list.
self.assertTrue(self.action1 in self.list)
self.assertFalse(self.action2 in self.list)
def len_test(self):
"""
Test the __len__ method
"""
# GIVEN: The list.
# WHEN: Do nothing.
# THEN: Check the length.
self.assertEqual(len(self.list), 0, "The length should be 0.")
# GIVEN: The list.
# WHEN: Append an action.
self.list.append(self.action1)
# THEN: Check the length.
self.assertEqual(len(self.list), 1, "The length should be 1.")
def append_test(self):
"""
Test the append() method
"""
# GIVEN: The list.
# WHEN: Append an action.
self.list.append(self.action1)
self.list.append(self.action2)
# THEN: Check if the actions are in the list and check if they have the correct weights.
self.assertTrue(self.action1 in self.list)
self.assertTrue(self.action2 in self.list)
self.assertEqual(self.list.actions[0], (0, self.action1))
self.assertEqual(self.list.actions[1], (1, self.action2))
def add_test(self):
"""
Test the add() method
"""
# GIVEN: The list and weights.
action1_weight = 42
action2_weight = 41
# WHEN: Add actions and their weights.
self.list.add(self.action1, action1_weight)
self.list.add(self.action2, action2_weight)
# THEN: Check if they were added and have the specified weights.
self.assertTrue(self.action1 in self.list)
self.assertTrue(self.action2 in self.list)
# Now check if action1 is second and action2 is first (due to their weights).
self.assertEqual(self.list.actions[0], (41, self.action2))
self.assertEqual(self.list.actions[1], (42, self.action1))
def remove_test(self):
"""
Test the remove() method
"""
# GIVEN: The list
self.list.append(self.action1)
# WHEN: Delete an item from the list.
self.list.remove(self.action1)
# THEN: Now the element should not be in the list anymore.
self.assertFalse(self.action1 in self.list)
# THEN: Check if an exception is raised when trying to remove a not present action.
self.assertRaises(ValueError, self.list.remove, self.action2)
class TestActionList(TestCase, TestMixin):
"""
Test the ActionList class

Some files were not shown because too many files have changed in this diff Show More