This commit is contained in:
Tomas Groth 2014-05-21 17:56:57 +02:00
commit d9c6e2a228
125 changed files with 1411 additions and 476 deletions

View File

@ -6,6 +6,8 @@
*.ropeproject
*.e4*
.eric4project
.komodotools
*.komodoproject
list
openlp.org 2.0.e4*
documentation/build/html
@ -30,3 +32,4 @@ tests.kdev4
*.orig
__pycache__
*.dll
.directory

View File

@ -5,6 +5,8 @@ recursive-include openlp *.html
recursive-include openlp *.js
recursive-include openlp *.css
recursive-include openlp *.png
recursive-include openlp *.ps
recursive-include openlp *.json
recursive-include documentation *
recursive-include resources *
recursive-include scripts *

View File

@ -1,16 +1,15 @@
OpenLP 2.0
==========
OpenLP
======
You're probably reading this because you've just downloaded the source code for
OpenLP 2.0. If you are looking for the installer file, please go to the download
OpenLP. If you are looking for the installer file, please go to the download
page on the web site::
http://openlp.org/en/download.html
http://openlp.org/download
If you're looking for how to contribute to OpenLP, then please look at the
OpenLP wiki::
http://wiki.openlp.org/
Thanks for downloading OpenLP 2.0!
Thanks for downloading OpenLP!

View File

@ -110,7 +110,7 @@ class AppLocation(object):
:param extension:
Defaults to *None*. The extension to search for. For example::
u'.png'
'.png'
"""
path = AppLocation.get_data_path()
if section:

View File

@ -68,8 +68,7 @@ class Settings(QtCore.QSettings):
``__obsolete_settings__``
Each entry is structured in the following way::
(u'general/enable slide loop', u'advanced/slide limits',
[(SlideLimits.Wrap, True), (SlideLimits.End, False)])
('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
The first entry is the *old key*; it will be removed.

View File

@ -296,8 +296,7 @@ def create_separated_list(string_list):
:param string_list: List of unicode strings
"""
if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and \
LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
return QtCore.QLocale().createSeparatedList(string_list)
if not string_list:
return ''

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

@ -129,7 +129,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
class MyPlugin(Plugin):
def __init__(self):
super(MyPlugin, self).__init__('MyPlugin', version=u'0.1')
super(MyPlugin, self).__init__('MyPlugin', version='0.1')
:param name: Defaults to *None*. The name of the plugin.
:param default_settings: A dict containing the plugin's settings. The value to each key is the default value

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

@ -63,8 +63,7 @@ class ScreenList(object):
"""
Initialise the screen list.
``desktop``
A ``QDesktopWidget`` object.
:param desktop: A QDesktopWidget object.
"""
screen_list = cls()
screen_list.desktop = desktop
@ -136,7 +135,7 @@ class ScreenList(object):
Returns a list with the screens. This should only be used to display
available screens to the user::
[u'Screen 1 (primary)', u'Screen 2']
['Screen 1 (primary)', 'Screen 2']
"""
screen_list = []
for screen in self.screen_list:
@ -153,9 +152,9 @@ class ScreenList(object):
:param screen: A dict with the screen properties::
{
u'primary': True,
u'number': 0,
u'size': PyQt4.QtCore.QRect(0, 0, 1024, 768)
'primary': True,
'number': 0,
'size': PyQt4.QtCore.QRect(0, 0, 1024, 768)
}
"""
log.info('Screen %d found with resolution %s' % (screen['number'], screen['size']))

View File

@ -44,7 +44,7 @@ class Ui_AboutDialog(object):
Set up the UI for the dialog.
"""
about_dialog.setObjectName('about_dialog')
about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg'))
self.about_dialog_layout = QtGui.QVBoxLayout(about_dialog)
self.about_dialog_layout.setObjectName('about_dialog_layout')
self.logo_label = QtGui.QLabel(about_dialog)

View File

@ -30,7 +30,7 @@
The About dialog.
"""
from PyQt4 import QtCore, QtGui
from PyQt4 import QtGui
from .aboutdialog import Ui_AboutDialog
from openlp.core.lib import translate

View File

@ -30,9 +30,9 @@
The GUI widgets of the exception dialog.
"""
from PyQt4 import QtCore, QtGui
from PyQt4 import QtGui
from openlp.core.lib import translate
from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button, create_button_box
@ -45,6 +45,7 @@ class Ui_ExceptionDialog(object):
Set up the UI.
"""
exception_dialog.setObjectName('exception_dialog')
exception_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.exception_layout = QtGui.QVBoxLayout(exception_dialog)
self.exception_layout.setObjectName('exception_layout')
self.message_layout = QtGui.QHBoxLayout()

View File

@ -31,7 +31,7 @@ The UI widgets for the rename dialog
"""
from PyQt4 import QtCore, QtGui
from openlp.core.lib import translate
from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +44,7 @@ class Ui_FileRenameDialog(object):
Set up the UI
"""
file_rename_dialog.setObjectName('file_rename_dialog')
file_rename_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
file_rename_dialog.resize(300, 10)
self.dialog_layout = QtGui.QGridLayout(file_rename_dialog)
self.dialog_layout.setObjectName('dialog_layout')

View File

@ -114,10 +114,10 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
Run the wizard.
"""
self.setDefaults()
self.set_defaults()
return QtGui.QWizard.exec_(self)
def setDefaults(self):
def set_defaults(self):
"""
Set up display at start of theme edit.
"""
@ -199,8 +199,8 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
self.no_internet_label.setText(self.no_internet_text + self.cancelWizardText)
elif page_id == FirstTimePage.Defaults:
self.theme_combo_box.clear()
for iter in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(iter)
for index in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(index)
if item.checkState() == QtCore.Qt.Checked:
self.theme_combo_box.addItem(item.text())
if self.has_run_wizard:
@ -292,13 +292,9 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties):
"""
themes = self.config.get('themes', 'files')
themes = themes.split(',')
for theme in themes:
filename = self.config.get('theme_%s' % theme, 'filename')
for index, theme in enumerate(themes):
screenshot = self.config.get('theme_%s' % theme, 'screenshot')
for index in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(index)
if item.data(QtCore.Qt.UserRole) == filename:
break
item = self.themes_list_widget.item(index)
item.setIcon(build_icon(os.path.join(gettempdir(), 'openlp', screenshot)))
def _get_file_size(self, url):

View File

@ -32,6 +32,7 @@ The UI widgets of the language selection dialog.
from PyQt4 import QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +45,7 @@ class Ui_FirstTimeLanguageDialog(object):
Set up the UI.
"""
language_dialog.setObjectName('language_dialog')
language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
language_dialog.resize(300, 50)
self.dialog_layout = QtGui.QVBoxLayout(language_dialog)
self.dialog_layout.setContentsMargins(8, 8, 8, 8)

View File

@ -34,6 +34,7 @@ from PyQt4 import QtCore, QtGui
import sys
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import add_welcome_page
@ -60,6 +61,7 @@ class Ui_FirstTimeWizard(object):
Set up the UI.
"""
first_time_wizard.setObjectName('first_time_wizard')
first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
first_time_wizard.resize(550, 386)
first_time_wizard.setModal(True)
first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle)

View File

@ -45,6 +45,7 @@ class Ui_FormattingTagDialog(object):
Set up the UI
"""
formatting_tag_dialog.setObjectName('formatting_tag_dialog')
formatting_tag_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
formatting_tag_dialog.resize(725, 548)
self.list_data_grid_layout = QtGui.QVBoxLayout(formatting_tag_dialog)
self.list_data_grid_layout.setMargin(8)

View File

@ -89,7 +89,7 @@ class Ui_MainWindow(object):
Set up the user interface
"""
main_window.setObjectName('MainWindow')
main_window.setWindowIcon(build_icon(':/icon/openlp-logo-64x64.png'))
main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg'))
main_window.setDockNestingEnabled(True)
# Set up the main container, which contains all the other form widgets.
self.main_content = QtGui.QWidget(main_window)
@ -320,14 +320,14 @@ class Ui_MainWindow(object):
# i18n add Language Actions
add_actions(self.settings_language_menu, (self.auto_language_item, None))
add_actions(self.settings_language_menu, self.language_group.actions())
# Order things differently in OS X so that Preferences menu item in the
# app menu is correct (this gets picked up automatically by Qt).
# Qt on OS X looks for keywords in the menu items title to determine which menu items get added to the main
# menu. If we are running on Mac OS X the menu items whose title contains those keywords but don't belong in the
# main menu need to be marked as such with QAction.NoRole.
if sys.platform == 'darwin':
add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
None, self.settings_configure_item, self.settings_shortcuts_item, self.formatting_tag_item))
else:
add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
None, self.formatting_tag_item, self.settings_shortcuts_item, self.settings_configure_item))
self.settings_shortcuts_item.setMenuRole(QtGui.QAction.NoRole)
self.formatting_tag_item.setMenuRole(QtGui.QAction.NoRole)
add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(),
None, self.formatting_tag_item, self.settings_shortcuts_item, self.settings_configure_item))
add_actions(self.tools_menu, (self.tools_add_tool_item, None))
add_actions(self.tools_menu, (self.tools_open_data_folder, None))
add_actions(self.tools_menu, (self.tools_first_time_wizard, None))
@ -1334,7 +1334,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
if self.copy_data:
log.info('Copying data to new path')
try:
self.showStatusMessage(
self.show_status_message(
translate('OpenLP.MainWindow', 'Copying OpenLP data to new data directory location - %s '
'- Please wait for copy to finish').replace('%s', self.new_data_path))
dir_util.copy_tree(old_data_path, self.new_data_path)
@ -1364,8 +1364,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
args = []
for a in self.arguments:
args.extend([a])
for arg in args:
filename = arg
for filename in args:
if not isinstance(filename, str):
filename = str(filename, sys.getfilesystemencoding())
if filename.endswith(('.osz', '.oszl')):

View File

@ -29,8 +29,6 @@
"""
The :mod:`~openlp.core.ui.media.mediaplayer` module contains the MediaPlayer class.
"""
import os
from openlp.core.common import RegistryProperties
from openlp.core.ui.media import MediaState

View File

@ -33,10 +33,8 @@ import logging
import mimetypes
from datetime import datetime
from PyQt4 import QtGui
from PyQt4.phonon import Phonon
from openlp.core.common import Settings
from openlp.core.lib import translate
from openlp.core.ui.media import MediaState

View File

@ -34,6 +34,7 @@ from distutils.version import LooseVersion
import logging
import os
import sys
import threading
from PyQt4 import QtGui
@ -206,7 +207,9 @@ class VlcPlayer(MediaPlayer):
controller = display.controller
start_time = 0
log.debug('vlc play')
display.vlc_media_player.play()
if self.state != MediaState.Paused and controller.media_info.start_time > 0:
start_time = controller.media_info.start_time
threading.Thread(target=display.vlc_media_player.play).start()
if not self.media_state_wait(display, vlc.State.Playing):
return False
if self.state != MediaState.Paused and controller.media_info.start_time > 0:
@ -254,7 +257,7 @@ class VlcPlayer(MediaPlayer):
"""
Stop the current item
"""
display.vlc_media_player.stop()
threading.Thread(target=display.vlc_media_player.stop).start()
self.state = MediaState.Stopped
def volume(self, display, vol):

View File

@ -32,6 +32,7 @@ The UI widgets of the plugin view dialog
from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +45,7 @@ class Ui_PluginViewDialog(object):
Set up the UI
"""
pluginViewDialog.setObjectName('pluginViewDialog')
pluginViewDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
pluginViewDialog.setWindowModality(QtCore.Qt.ApplicationModal)
self.plugin_layout = QtGui.QVBoxLayout(pluginViewDialog)
self.plugin_layout.setObjectName('plugin_layout')

View File

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

View File

@ -56,6 +56,7 @@ class Ui_PrintServiceDialog(object):
Set up the UI
"""
print_service_dialog.setObjectName('print_service_dialog')
print_service_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
print_service_dialog.resize(664, 594)
self.main_layout = QtGui.QVBoxLayout(print_service_dialog)
self.main_layout.setSpacing(0)

View File

@ -242,7 +242,7 @@ class PrintServiceForm(QtGui.QDialog, Ui_PrintServiceDialog, RegistryProperties)
Creates a html element. If ``text`` is given, the element's text will set and if a ``parent`` is given,
the element is appended.
:param tag: The html tag, e. g. ``u'span'``. Defaults to ``None``.
:param tag: The html tag, e. g. ``'span'``. Defaults to ``None``.
:param text: The text for the tag. Defaults to ``None``.
:param parent: The parent element. Defaults to ``None``.
:param classId: Value for the class attribute

View File

@ -32,6 +32,7 @@ The UI widgets for the service item edit dialog
from PyQt4 import QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box, create_button
@ -44,6 +45,7 @@ class Ui_ServiceItemEditDialog(object):
Set up the UI
"""
serviceItemEditDialog.setObjectName('serviceItemEditDialog')
serviceItemEditDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.dialog_layout = QtGui.QGridLayout(serviceItemEditDialog)
self.dialog_layout.setContentsMargins(8, 8, 8, 8)
self.dialog_layout.setSpacing(8)

View File

@ -45,8 +45,8 @@ class Ui_SettingsDialog(object):
Set up the UI
"""
settings_dialog.setObjectName('settings_dialog')
settings_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
settings_dialog.resize(800, 500)
settings_dialog.setWindowIcon(build_icon(':/system/system_settings.png'))
self.dialog_layout = QtGui.QGridLayout(settings_dialog)
self.dialog_layout.setObjectName('dialog_layout')
self.dialog_layout.setMargin(8)

View File

@ -66,6 +66,7 @@ class Ui_ShortcutListDialog(object):
Set up the UI
"""
shortcutListDialog.setObjectName('shortcutListDialog')
shortcutListDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
shortcutListDialog.resize(500, 438)
self.shortcut_list_layout = QtGui.QVBoxLayout(shortcutListDialog)
self.shortcut_list_layout.setObjectName('shortcut_list_layout')

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

@ -32,6 +32,7 @@ The UI widgets for the time dialog
from PyQt4 import QtCore, QtGui
from openlp.core.common import UiStrings, translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +45,7 @@ class Ui_StartTimeDialog(object):
Set up the UI
"""
StartTimeDialog.setObjectName('StartTimeDialog')
StartTimeDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
StartTimeDialog.resize(350, 10)
self.dialog_layout = QtGui.QGridLayout(StartTimeDialog)
self.dialog_layout.setObjectName('dialog_layout')

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)

View File

@ -32,6 +32,7 @@ The layout of the theme
from PyQt4 import QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +45,7 @@ class Ui_ThemeLayoutDialog(object):
Set up the UI
"""
themeLayoutDialog.setObjectName('themeLayoutDialogDialog')
themeLayoutDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.preview_layout = QtGui.QVBoxLayout(themeLayoutDialog)
self.preview_layout.setObjectName('preview_layout')
self.preview_area = QtGui.QWidget(themeLayoutDialog)

View File

@ -190,7 +190,7 @@ class ThemesTab(SettingsTab):
:param theme_list: The list of available themes::
[u'Bible Theme', u'Song Theme']
['Bible Theme', 'Song Theme']
"""
# Reload as may have been triggered by the ThemeManager.
self.global_theme = Settings().value(self.settings_section + '/global theme')

View File

@ -46,6 +46,7 @@ class Ui_ThemeWizard(object):
Set up the UI
"""
themeWizard.setObjectName('OpenLP.ThemeWizard')
themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
themeWizard.setModal(True)
themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle)
themeWizard.setOptions(QtGui.QWizard.IndependentPages |

View File

@ -118,6 +118,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
"""
Set up the wizard UI.
"""
self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.setModal(True)
self.setWizardStyle(QtGui.QWizard.ModernStyle)
self.setOptions(QtGui.QWizard.IndependentPages |
@ -197,7 +198,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
"""
Run the wizard.
"""
self.setDefaults()
self.set_defaults()
return QtGui.QWizard.exec_(self)
def reject(self):
@ -279,7 +280,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties):
:param filters: The file extension filters. It should contain the file description
as well as the file extension. For example::
u'OpenLP 2.0 Databases (*.sqlite)'
'OpenLP 2.0 Databases (*.sqlite)'
"""
if filters:
filters += ';;'

View File

@ -113,7 +113,7 @@ def get_application_version():
"""
Returns the application version of the running instance of OpenLP::
{u'full': u'1.9.4-bzr1249', u'version': u'1.9.4', u'build': u'bzr1249'}
{'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
"""
global APPLICATION_VERSION
if APPLICATION_VERSION:

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

@ -32,7 +32,7 @@ other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned

View File

@ -46,7 +46,7 @@ class Ui_AlertDialog(object):
"""
alert_dialog.setObjectName('alert_dialog')
alert_dialog.resize(400, 300)
alert_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
alert_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
self.alert_dialog_layout = QtGui.QGridLayout(alert_dialog)
self.alert_dialog_layout.setObjectName('alert_dialog_layout')
self.alert_text_layout = QtGui.QFormLayout()

View File

@ -33,7 +33,7 @@ other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned

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

@ -30,12 +30,14 @@
from PyQt4 import QtCore, QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
class Ui_BookNameDialog(object):
def setupUi(self, book_name_dialog):
book_name_dialog.setObjectName('book_name_dialog')
book_name_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
book_name_dialog.resize(400, 271)
self.book_name_layout = QtGui.QVBoxLayout(book_name_dialog)
self.book_name_layout.setSpacing(8)

View File

@ -39,8 +39,8 @@ from openlp.plugins.bibles.lib.db import BiblesResourcesDB
class Ui_EditBibleDialog(object):
def setupUi(self, edit_bible_dialog):
edit_bible_dialog.setObjectName('edit_bible_dialog')
edit_bible_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_bible_dialog.resize(520, 400)
edit_bible_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
edit_bible_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_bible_dialog)
self.dialog_layout.setSpacing(8)

View File

@ -30,12 +30,14 @@
from PyQt4 import QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
class Ui_LanguageDialog(object):
def setupUi(self, language_dialog):
language_dialog.setObjectName('language_dialog')
language_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
language_dialog.resize(400, 165)
self.language_layout = QtGui.QVBoxLayout(language_dialog)
self.language_layout.setSpacing(8)

View File

@ -262,7 +262,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
For example::
[(u'John', 3, 16, 18), (u'John', 4, 1, 1)]
[('John', 3, 16, 18), ('John', 4, 1, 1)]
**Reference string details:**
@ -311,7 +311,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
``(?P<to_verse>[0-9]+)``
The ``to_verse`` reference is equivalent to group 2.
The full reference is matched against get_reference_match(u'full'). This regular expression looks like this:
The full reference is matched against get_reference_match('full'). This regular expression looks like this:
``^\s*(?!\s)(?P<book>[\d]*[^\d]+)(?<!\s)\s*``
The ``book`` group starts with the first non-whitespace character. There are optional leading digits followed by

View File

@ -405,7 +405,7 @@ class BiblesTab(SettingsTab):
:param theme_list:
The list of available themes::
[u'Bible Theme', u'Song Theme']
['Bible Theme', 'Song Theme']
"""
self.bible_theme_combo_box.clear()
self.bible_theme_combo_box.addItem('')

View File

@ -370,17 +370,16 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
This is probably the most used function. It retrieves the list of
verses based on the user's query.
:param reference_list: This is the list of references the media manager item wants. It is
a list of tuples, with the following format::
:param reference_list: This is the list of references the media manager item wants. It is a list of tuples, with
the following format::
(book_reference_id, chapter, start_verse, end_verse)
Therefore, when you are looking for multiple items, simply break
them up into references like this, bundle them into a list. This
function then runs through the list, and returns an amalgamated
list of ``Verse`` objects. For example::
Therefore, when you are looking for multiple items, simply break them up into references like this, bundle
them into a list. This function then runs through the list, and returns an amalgamated list of ``Verse``
objects. For example::
[(u'35', 1, 1, 1), (u'35', 2, 2, 3)]
[('35', 1, 1, 1), ('35', 2, 2, 3)]
:param show_error:
"""
log.debug('BibleDB.get_verses("%s")' % reference_list)

View File

@ -534,7 +534,7 @@ class HTTPBible(BibleDB, RegistryProperties):
them into a list. This function then runs through the list, and returns an amalgamated list of ``Verse``
objects. For example::
[(u'35', 1, 1, 1), (u'35', 2, 2, 3)]
[('35', 1, 1, 1), ('35', 2, 2, 3)]
"""
log.debug('HTTPBible.get_verses("%s")', reference_list)
for reference in reference_list:

View File

@ -54,19 +54,19 @@ class BibleFormat(object):
WebDownload = 3
@staticmethod
def get_class(format):
def get_class(bible_format):
"""
Return the appropriate implementation class.
:param format: The Bible format.
:param bible_format: The Bible format.
"""
if format == BibleFormat.OSIS:
if bible_format == BibleFormat.OSIS:
return OSISBible
elif format == BibleFormat.CSV:
elif bible_format == BibleFormat.CSV:
return CSVBible
elif format == BibleFormat.OpenSong:
elif bible_format == BibleFormat.OpenSong:
return OpenSongBible
elif format == BibleFormat.WebDownload:
elif bible_format == BibleFormat.WebDownload:
return HTTPBible
else:
return None

View File

@ -60,7 +60,6 @@ class BibleMediaItem(MediaManagerItem):
log.info('Bible Media Item loaded')
def __init__(self, parent, plugin):
self.icon_path = 'songs/song'
self.lock_icon = build_icon(':/bibles/bibles_search_lock.png')
self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png')
MediaManagerItem.__init__(self, parent, plugin)
@ -172,6 +171,7 @@ class BibleMediaItem(MediaManagerItem):
self.page_layout.addWidget(tab)
tab.setVisible(False)
lock_button.toggled.connect(self.on_lock_button_toggled)
second_combo_box.currentIndexChanged.connect(self.on_second_bible_combobox_index_changed)
setattr(self, prefix + 'VersionLabel', version_label)
setattr(self, prefix + 'VersionComboBox', version_combo_box)
setattr(self, prefix + 'SecondLabel', second_label)
@ -263,11 +263,15 @@ class BibleMediaItem(MediaManagerItem):
def config_update(self):
log.debug('config_update')
if Settings().value(self.settings_section + '/second bibles'):
self.quickSecondLabel.setVisible(True)
self.quickSecondComboBox.setVisible(True)
self.advancedSecondLabel.setVisible(True)
self.advancedSecondComboBox.setVisible(True)
self.quickSecondLabel.setVisible(True)
self.quickSecondComboBox.setVisible(True)
else:
self.quickSecondLabel.setVisible(False)
self.quickSecondComboBox.setVisible(False)
self.advancedSecondLabel.setVisible(False)
self.advancedSecondComboBox.setVisible(False)
self.quickSecondLabel.setVisible(False)
@ -360,8 +364,7 @@ class BibleMediaItem(MediaManagerItem):
combo boxes on the 'Advanced Search' Tab. This is not of any importance of the 'Quick Search' Tab.
:param bible: The bible to initialise (unicode).
:param last_book_id: The "book reference id" of the book which is chosen at the moment.
(int)
:param last_book_id: The "book reference id" of the book which is chosen at the moment. (int)
"""
log.debug('initialise_advanced_bible %s, %s', bible, last_book_id)
book_data = self.plugin.manager.get_books(bible)
@ -421,9 +424,8 @@ class BibleMediaItem(MediaManagerItem):
def update_auto_completer(self):
"""
This updates the bible book completion list for the search field. The
completion depends on the bible. It is only updated when we are doing a
reference search, otherwise the auto completion list is removed.
This updates the bible book completion list for the search field. The completion depends on the bible. It is
only updated when we are doing a reference search, otherwise the auto completion list is removed.
"""
log.debug('update_auto_completer')
# Save the current search type to the configuration.
@ -461,6 +463,17 @@ class BibleMediaItem(MediaManagerItem):
books.sort(key=get_locale_key)
set_case_insensitive_completer(books, self.quick_search_edit)
def on_second_bible_combobox_index_changed(self, selection):
"""
Activate the style combobox only when no second bible is selected
"""
if selection == 0:
self.quickStyleComboBox.setEnabled(True)
self.advancedStyleComboBox.setEnabled(True)
else:
self.quickStyleComboBox.setEnabled(False)
self.advancedStyleComboBox.setEnabled(False)
def on_import_click(self):
if not hasattr(self, 'import_wizard'):
self.import_wizard = BibleImportForm(self, self.plugin.manager, self.plugin)
@ -593,8 +606,7 @@ class BibleMediaItem(MediaManagerItem):
:param range_from: The first number of the range (int).
:param range_to: The last number of the range (int).
:param combo: The combo box itself (QComboBox).
:param restore: If True, then the combo's currentText will be restored after
adjusting (if possible).
:param restore: If True, then the combo's currentText will be restored after adjusting (if possible).
"""
log.debug('adjust_combo_box %s, %s, %s', combo, range_from, range_to)
if restore:
@ -640,8 +652,8 @@ class BibleMediaItem(MediaManagerItem):
def on_quick_search_button(self):
"""
Does a quick search and saves the search results. Quick search can
either be "Reference Search" or "Text Search".
Does a quick search and saves the search results. Quick search can either be "Reference Search" or
"Text Search".
"""
log.debug('Quick Search Button clicked')
self.quickSearchButton.setEnabled(False)
@ -696,8 +708,7 @@ class BibleMediaItem(MediaManagerItem):
def display_results(self, bible, second_bible=''):
"""
Displays the search results in the media manager. All data needed for
further action is saved for/in each row.
Displays the search results in the media manager. All data needed for further action is saved for/in each row.
"""
items = self.build_display_results(bible, second_bible, self.search_results)
for bible_verse in items:
@ -708,8 +719,7 @@ class BibleMediaItem(MediaManagerItem):
def build_display_results(self, bible, second_bible, search_results):
"""
Displays the search results in the media manager. All data needed for
further action is saved for/in each row.
Displays the search results in the media manager. All data needed for further action is saved for/in each row.
"""
verse_separator = get_reference_separator('sep_v_display')
version = self.plugin.manager.get_meta_data(bible, 'name').value
@ -837,7 +847,6 @@ class BibleMediaItem(MediaManagerItem):
# If there are no more items we check whether we have to add bible_text.
if bible_text:
raw_slides.append(bible_text.lstrip())
bible_text = ''
# Service Item: Capabilities
if self.settings.layout_style == LayoutStyle.Continuous and not second_bible:
# Split the line but do not replace line breaks in renderer.
@ -859,9 +868,8 @@ class BibleMediaItem(MediaManagerItem):
def format_title(self, start_bitem, old_bitem):
"""
This method is called, when we have to change the title, because
we are at the end of a verse range. E. g. if we want to add
Genesis 1:1-6 as well as Daniel 2:14.
This method is called, when we have to change the title, because we are at the end of a verse range. E. g. if we
want to add Genesis 1:1-6 as well as Daniel 2:14.
:param start_bitem: The first item of a range.
:param old_bitem: The last item of a range.
@ -891,10 +899,8 @@ class BibleMediaItem(MediaManagerItem):
def check_title(self, bitem, old_bitem):
"""
This method checks if we are at the end of an verse range. If that is
the case, we return True, otherwise False. E. g. if we added
Genesis 1:1-6, but the next verse is Daniel 2:14, we return True.
This method checks if we are at the end of an verse range. If that is the case, we return True, otherwise False.
E. g. if we added Genesis 1:1-6, but the next verse is Daniel 2:14, we return True.
:param bitem: The item we are dealing with at the moment.
:param old_bitem: The item we were previously dealing with.
@ -918,20 +924,17 @@ class BibleMediaItem(MediaManagerItem):
return True
elif old_chapter + 1 == chapter and (verse != 1 or old_verse !=
self.plugin.manager.get_verse_count(old_bible, old_book, old_chapter)):
# We are in the following chapter, but the last verse was not the
# last verse of the chapter or the current verse is not the
# first one of the chapter.
# We are in the following chapter, but the last verse was not the last verse of the chapter or the current
# verse is not the first one of the chapter.
return True
return False
def format_verse(self, old_chapter, chapter, verse):
"""
Formats and returns the text, each verse starts with, for the given
chapter and verse. The text is either surrounded by round, square,
Formats and returns the text, each verse starts with, for the given chapter and verse. The text is either
surrounded by round, square, curly brackets or no brackets at all. For example::
curly brackets or no brackets at all. For example::
u'{su}1:1{/su}'
'{su}1:1{/su}'
:param old_chapter: The previous verse's chapter number (int).
:param chapter: The chapter number (int).

View File

@ -41,8 +41,8 @@ class Ui_CustomEditDialog(object):
:param custom_edit_dialog: The Dialog
"""
custom_edit_dialog.setObjectName('custom_edit_dialog')
custom_edit_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
custom_edit_dialog.resize(450, 350)
custom_edit_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
self.dialog_layout = QtGui.QVBoxLayout(custom_edit_dialog)
self.dialog_layout.setObjectName('dialog_layout')
self.title_layout = QtGui.QHBoxLayout()

View File

@ -197,7 +197,6 @@ class EditCustomForm(QtGui.QDialog, Ui_CustomEditDialog):
self.slide_list_view.clear()
self.slide_list_view.addItems(slides)
else:
old_slides = []
old_row = self.slide_list_view.currentRow()
# Create a list with all (old/unedited) slides.
old_slides = [self.slide_list_view.item(row).text() for row in range(self.slide_list_view.count())]

View File

@ -30,13 +30,14 @@
from PyQt4 import QtGui
from openlp.core.common import UiStrings, translate
from openlp.core.lib import SpellTextEdit
from openlp.core.lib import SpellTextEdit, build_icon
from openlp.core.lib.ui import create_button, create_button_box
class Ui_CustomSlideEditDialog(object):
def setupUi(self, custom_slide_edit_dialog):
custom_slide_edit_dialog.setObjectName('custom_slide_edit_dialog')
custom_slide_edit_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
custom_slide_edit_dialog.resize(350, 300)
self.dialog_layout = QtGui.QVBoxLayout(custom_slide_edit_dialog)
self.slide_text_edit = SpellTextEdit(self)

View File

@ -32,7 +32,7 @@ other class holds all the functional code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named ``Ui_<name>Dialog``. It is a slightly
modified version of the class that the ``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()`` function for translating strings.
converting most strings from "" to '' and using OpenLP's ``translate()`` function for translating strings.
The second class, commonly known as the **Form** class, is typically named ``<name>Form``. This class is the one which
is instantiated and used. It uses dual inheritance to inherit from (usually) QtGui.QDialog and the Ui class mentioned

View File

@ -31,7 +31,7 @@ The :mod:`db` module provides the database and schema that is the backend for th
"""
from sqlalchemy import Column, ForeignKey, Table, types
from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db

View File

@ -353,7 +353,7 @@ class ImageMediaItem(MediaManagerItem):
icon = build_icon(thumb)
else:
icon = create_thumb(imageFile.filename, thumb)
item_name = QtGui.QTreeWidgetItem(filename)
item_name = QtGui.QTreeWidgetItem([filename])
item_name.setText(0, filename)
item_name.setIcon(0, icon)
item_name.setToolTip(0, imageFile.filename)

View File

@ -379,7 +379,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
"""
media = Settings().value(self.settings_section + '/media files')
media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1]))
extension = []
if type == MediaType.Audio:
extension = self.media_controller.audio_extensions_list
else:

View File

@ -354,7 +354,7 @@ class PresentationController(object):
class MyPresentationController(PresentationController):
def __init__(self, plugin):
PresentationController.__init(
self, plugin, u'My Presenter App')
self, plugin, 'My Presenter App')
:param plugin: Defaults to *None*. The presentationplugin object
:param name: Name of the application, to appear in the application

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):
"""
@ -104,9 +120,11 @@ class RemotesPlugin(Plugin):
def config_update(self):
"""
Called when Config is changed to restart the server on new address or port
Called when Config is changed to requests a restart with the server on new address or port
"""
log.debug('remote config changed')
self.finalise()
time.sleep(0.5)
self.initialise()
QtGui.QMessageBox.information(self.main_window,
translate('RemotePlugin', 'Server Config Change'),
translate('RemotePlugin', 'Server configuration changes will require a restart '
'to take effect.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))

View File

@ -34,7 +34,7 @@ code, like slots and loading and saving.
The first class, commonly known as the **Dialog** class, is typically named
``Ui_<name>Dialog``. It is a slightly modified version of the class that the
``pyuic4`` command produces from Qt4's .ui file. Typical modifications will be
converting most strings from "" to u'' and using OpenLP's ``translate()``
converting most strings from "" to '' and using OpenLP's ``translate()``
function for translating strings.
The second class, commonly known as the **Form** class, is typically named

View File

@ -43,8 +43,8 @@ class Ui_AuthorsDialog(object):
Set up the UI for the dialog.
"""
authors_dialog.setObjectName('authors_dialog')
authors_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
authors_dialog.resize(300, 10)
authors_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
authors_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(authors_dialog)
self.dialog_layout.setObjectName('dialog_layout')

View File

@ -264,7 +264,7 @@ class DuplicateSongRemovalForm(OpenLPWizard, RegistryProperties):
self.break_search = True
self.plugin.media_item.on_search_text_button_clicked()
def setDefaults(self):
def set_defaults(self):
"""
Set default form values for the song import wizard.
"""

View File

@ -43,8 +43,8 @@ class Ui_EditSongDialog(object):
"""
def setupUi(self, edit_song_dialog):
edit_song_dialog.setObjectName('edit_song_dialog')
edit_song_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_song_dialog.resize(650, 400)
edit_song_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
edit_song_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_song_dialog)
self.dialog_layout.setSpacing(8)
@ -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
@ -107,6 +107,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.audio_list_widget.setAlternatingRowColors(True)
self.find_verse_split = re.compile('---\[\]---\n', re.UNICODE)
self.whitespace = re.compile(r'\W+', re.UNICODE)
self.find_tags = re.compile(u'\{/?\w+\}', re.UNICODE)
def _load_objects(self, cls, combo, cache):
"""
@ -122,12 +123,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 +218,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())
@ -234,8 +235,57 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
self.manager.save_object(book)
else:
return False
# Validate tags (lp#1199639)
misplaced_tags = []
verse_tags = []
for i in range(self.verse_list_widget.rowCount()):
item = self.verse_list_widget.item(i, 0)
tags = self.find_tags.findall(item.text())
field = item.data(QtCore.Qt.UserRole)
verse_tags.append(field)
if not self._validate_tags(tags):
misplaced_tags.append('%s %s' % (VerseType.translated_name(field[0]), field[1:]))
if misplaced_tags:
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm',
'There are misplaced formatting tags in the following verses:\n\n%s\n\n'
'Please correct these tags before continuing.' % ', '.join(misplaced_tags)))
return False
for tag in verse_tags:
if verse_tags.count(tag) > 26:
# lp#1310523: OpenLyrics allows only a-z variants of one verse:
# http://openlyrics.info/dataformat.html#verse-name
critical_error_message_box(message=translate(
'SongsPlugin.EditSongForm', 'You have %(count)s verses named %(name)s %(number)s. '
'You can have at most 26 verses with the same name' %
{'count': verse_tags.count(tag),
'name': VerseType.translated_name(tag[0]),
'number': tag[1:]}))
return False
return True
def _validate_tags(self, tags):
"""
Validates a list of tags
Deletes the first affiliated tag pair which is located side by side in the list
and call itself recursively with the shortened tag list.
If there is any misplaced tag in the list, either the length of the tag list is not even,
or the function won't find any tag pairs side by side.
If there is no misplaced tag, the length of the list will be zero on any recursive run.
:param tags: A list of tags
:return: True if the function can't find any mismatched tags. Else False.
"""
if len(tags) == 0:
return True
if len(tags) % 2 != 0:
return False
for i in range(len(tags)-1):
if tags[i+1] == "{/" + tags[i][1:]:
del tags[i:i+2]
return self._validate_tags(tags)
return False
def _process_lyrics(self):
"""
Process the lyric data entered by the user into the OpenLP XML format.
@ -302,6 +352,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 +513,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 +553,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 +571,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 +579,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
elif item > 0:
item_id = (self.authors_combo_box.itemData(item))
author = self.manager.get_object(Author, item_id)
if self.authors_list_view.findItems(str(author.display_name), QtCore.Qt.MatchExactly):
if self.authors_list_view.findItems(author.get_display_name(author_type), QtCore.Qt.MatchExactly):
critical_error_message_box(
message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.'))
else:
self._add_author_to_list(author)
self._add_author_to_list(author, author_type)
self.authors_combo_box.setCurrentIndex(0)
else:
QtGui.QMessageBox.warning(
@ -905,13 +963,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

@ -37,6 +37,7 @@ from openlp.plugins.songs.lib import VerseType
class Ui_EditVerseDialog(object):
def setupUi(self, edit_verse_dialog):
edit_verse_dialog.setObjectName('edit_verse_dialog')
edit_verse_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
edit_verse_dialog.resize(400, 400)
edit_verse_dialog.setModal(True)
self.dialog_layout = QtGui.QVBoxLayout(edit_verse_dialog)

View File

@ -122,8 +122,6 @@ class EditVerseForm(QtGui.QDialog, Ui_EditVerseDialog):
text = text[:position + 4]
match = VERSE_REGEX.match(text)
if match:
# TODO: Not used, remove?
# verse_tag = match.group(1)
try:
verse_num = int(match.group(2)) + 1
except ValueError:

View File

@ -42,10 +42,10 @@ class Ui_MediaFilesDialog(object):
Set up the user interface.
"""
media_files_dialog.setObjectName('media_files_dialog')
media_files_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
media_files_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
media_files_dialog.resize(400, 300)
media_files_dialog.setModal(True)
media_files_dialog.setWindowIcon(build_icon(':/icon/openlp-logo-16x16.png'))
self.files_vertical_layout = QtGui.QVBoxLayout(media_files_dialog)
self.files_vertical_layout.setSpacing(8)
self.files_vertical_layout.setMargin(8)

View File

@ -29,7 +29,7 @@
from PyQt4 import QtGui
from openlp.core.lib import translate
from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@ -42,6 +42,7 @@ class Ui_SongBookDialog(object):
Set up the user interface.
"""
song_book_dialog.setObjectName('song_book_dialog')
song_book_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_book_dialog.resize(300, 10)
self.dialog_layout = QtGui.QVBoxLayout(song_book_dialog)
self.dialog_layout.setObjectName('dialog_layout')

View File

@ -231,11 +231,11 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
"""
Opens a QFileDialog and writes the filenames to the given listbox.
:param title: The title of the dialog (unicode).
:param title: The title of the dialog (str).
:param listbox: A listbox (QListWidget).
:param filters: The file extension filters. It should contain the file descriptions
as well as the file extensions. For example::
u'SongBeamer Files (*.sng)'
:param filters: The file extension filters. It should contain the file descriptions as well as the file
extensions. For example::
'SongBeamer Files (*.sng)'
"""
if filters:
filters += ';;'
@ -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

@ -44,6 +44,7 @@ class Ui_SongMaintenanceDialog(object):
Set up the user interface for the song maintenance dialog
"""
song_maintenance_dialog.setObjectName('song_maintenance_dialog')
song_maintenance_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_maintenance_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
song_maintenance_dialog.resize(10, 350)
self.dialog_layout = QtGui.QGridLayout(song_maintenance_dialog)

View File

@ -319,8 +319,6 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog):
def on_search_finished(self):
"""
Slot which is called when the search is completed.
:param songs:
"""
self.application.process_events()
self.search_progress_bar.setVisible(False)

View File

@ -29,7 +29,7 @@
from PyQt4 import QtGui
from openlp.core.lib import translate
from openlp.core.lib import translate, build_icon
from openlp.core.lib.ui import create_button_box
@ -42,6 +42,7 @@ class Ui_TopicsDialog(object):
Set up the user interface for the topics dialog.
"""
topics_dialog.setObjectName('topics_dialog')
topics_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
topics_dialog.resize(300, 10)
self.dialog_layout = QtGui.QVBoxLayout(topics_dialog)
self.dialog_layout.setObjectName('dialog_layout')

View File

@ -206,14 +206,14 @@ class VerseType(object):
Return the VerseType for a given tag
:param verse_tag: The string to return a VerseType for
:param default: Default return value if no matching tag is found
:param default: Default return value if no matching tag is found (a valid VerseType or None)
:return: A VerseType of the tag
"""
verse_tag = verse_tag[0].lower()
for num, tag in enumerate(VerseType.tags):
if verse_tag == tag:
return num
if len(VerseType.names) > default:
if default in range(0, len(VerseType.names)) or default is None:
return default
else:
return VerseType.Other
@ -231,7 +231,7 @@ class VerseType(object):
for num, tag in enumerate(VerseType.translated_tags):
if verse_tag == tag:
return num
if len(VerseType.names) > default:
if default in range(0, len(VerseType.names)) or default is None:
return default
else:
return VerseType.Other
@ -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:
@ -434,7 +434,7 @@ def strip_rtf(text, default_encoding=None):
# Current font is the font tag we last met.
font = ''
# Character encoding is defined inside fonttable.
# font_table could contain eg u'0': u'cp1252'
# font_table could contain eg '0': u'cp1252'
font_table = {'': ''}
# Stack of things to keep track of when entering/leaving groups.
stack = []

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

@ -292,7 +292,7 @@ class EasySlidesImport(SongImport):
return True
def _extract_region(self, line):
# this was true already: line[0:7] == u'[region':
# this was true already: line[0:7] == '[region':
"""
Extract the region from text

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]+')
@ -74,12 +74,130 @@ class EasyWorshipSongImport(SongImport):
"""
def __init__(self, manager, **kwargs):
super(EasyWorshipSongImport, self).__init__(manager, **kwargs)
self.entry_error_log = ''
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 self.entry_error_log:
self.log_error(self.import_source,
translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
% (self.title, self.entry_error_log))
self.entry_error_log = ''
elif 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')
@ -169,86 +287,114 @@ class EasyWorshipSongImport(SongImport):
raw_record = db_file.read(record_size)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
self.title = self.get_field(fi_title).decode()
self.title = self.get_field(fi_title).decode('unicode-escape')
# Get remaining fields.
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
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()
self.copyright = copy.decode('unicode-escape')
if admin:
if copy:
self.copyright += ', '
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
'Administered by %s') % admin.decode()
'Administered by %s') % admin.decode('unicode-escape')
if ccli:
self.ccli_number = ccli.decode()
self.ccli_number = ccli.decode('unicode-escape')
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('unicode-escape')
else:
authors = ''
# Set the SongImport object members.
self.set_song_import_object(authors, words)
if self.stop_import_flag:
break
if not self.finish():
if self.entry_error_log:
self.log_error(self.import_source,
translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
% (self.title, self.entry_error_log))
self.entry_error_log = ''
elif not self.finish():
self.log_error(self.import_source)
db_file.close()
self.memo_file.close()
def set_song_import_object(self, authors, words):
"""
Set the SongImport object members.
:param authors: String with authons
:param words: Bytes with rtf-encoding
"""
if authors:
# 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 = None
decoded_words = None
try:
decoded_words = words.decode()
except UnicodeDecodeError:
# The unicode chars in the rtf was not escaped in the expected manor
self.entry_error_log = translate('SongsPlugin.EasyWorshipSongImport',
'Unexpected data formatting.')
return
result = strip_rtf(decoded_words, self.encoding)
if result is None:
self.entry_error_log = translate('SongsPlugin.EasyWorshipSongImport',
'No song text found.')
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 +431,7 @@ class EasyWorshipSongImport(SongImport):
Extract the field
:param field_desc_index: Field index value
:return:
:return: The field value
"""
field = self.fields[field_desc_index]
field_desc = self.field_descriptions[field_desc_index]
@ -323,3 +469,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

@ -36,15 +36,15 @@ from PyQt4 import QtCore, QtGui
from sqlalchemy.sql import or_
from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, check_item_selected, \
create_separated_list
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItem, ServiceItemContext, \
check_item_selected, create_separated_list
from openlp.core.lib.ui import create_widget_action
from openlp.plugins.songs.forms.editsongform import EditSongForm
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
@ -124,7 +124,8 @@ class SongMediaItem(MediaManagerItem):
log.debug('config_updated')
self.search_as_you_type = Settings().value(self.settings_section + '/search as type')
self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
self.add_song_from_service = Settings().value(self.settings_section + '/add song from service',)
self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
self.display_songbook = Settings().value(self.settings_section + '/display songbook')
def retranslateUi(self):
self.search_text_label.setText('%s:' % UiStrings().Search)
@ -234,8 +235,7 @@ class SongMediaItem(MediaManagerItem):
if song.temporary:
continue
author_list = [author.display_name for author in song.authors]
song_title = str(song.title)
song_detail = '%s (%s)' % (song_title, create_separated_list(author_list))
song_detail = '%s (%s)' % (song.title, create_separated_list(author_list)) if author_list else song.title
song_name = QtGui.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)
@ -464,23 +464,55 @@ 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 self.display_songbook and song.book:
item.raw_footer.append("%s #%s" % (song.book.name, song.song_number))
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):
"""
@ -489,16 +521,8 @@ class SongMediaItem(MediaManagerItem):
log.debug('service_load')
if self.plugin.status != PluginStatus.Active or not item.data_string:
return
if item.data_string['title'].find('@') == -1:
# FIXME: This file seems to be an old one (prior to 1.9.5), which means, that the search title
# (data_string[u'title']) is probably wrong. We add "@" to search title and hope that we do not add any
# duplicate. This should work for songs without alternate title.
temp = (re.compile(r'\W+', re.UNICODE).sub(' ', item.data_string['title'].strip()) + '@').strip().lower()
search_results = \
self.plugin.manager.get_all_objects(Song, Song.search_title == temp, Song.search_title.asc())
else:
search_results = self.plugin.manager.get_all_objects(
Song, Song.search_title == item.data_string['title'], Song.search_title.asc())
search_results = self.plugin.manager.get_all_objects(
Song, Song.search_title == item.data_string['title'], Song.search_title.asc())
edit_id = 0
add_song = True
if search_results:

View File

@ -171,7 +171,7 @@ class SongBeamerImport(SongImport):
:param line: The line in the file. It should consist of a tag and a value for this tag (unicode)::
u'#Title=Nearer my God to Thee'
'#Title=Nearer my God to Thee'
"""
tag_val = line.split('=', 1)
if len(tag_val) == 1:

View File

@ -146,14 +146,14 @@ class SongSelectImport(object):
try:
song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml')
except (TypeError, HTTPError) as e:
log.exception(u'Could not get song from SongSelect, %s', e)
log.exception('Could not get song from SongSelect, %s', e)
return None
if callback:
callback()
try:
lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml')
except (TypeError, HTTPError):
log.exception(u'Could not get lyrics from SongSelect')
log.exception('Could not get lyrics from SongSelect')
return None
if callback:
callback()

View File

@ -121,7 +121,7 @@ class SongShowPlusImport(SongImport):
null, verse_no, = struct.unpack("BB", song_data.read(2))
elif block_key == CUSTOM_VERSE:
null, verse_name_length, = struct.unpack("BB", song_data.read(2))
verse_name = song_data.read(verse_name_length)
verse_name = self.decode(song_data.read(verse_name_length))
length_descriptor_size, = struct.unpack("B", song_data.read(1))
log.debug(length_descriptor_size)
# Detect if/how long the length descriptor is
@ -147,7 +147,12 @@ class SongShowPlusImport(SongImport):
elif block_key == COPYRIGHT:
self.add_copyright(self.decode(data))
elif block_key == CCLI_NO:
self.ccli_number = int(data)
# Try to get the CCLI number even if the field contains additional text
match = re.search(r'\d+', self.decode(data))
if match:
self.ccli_number = int(match.group())
else:
log.warn("Can't parse CCLI Number from string: %s" % self.decode(data))
elif block_key == VERSE:
self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Verse], verse_no))
elif block_key == CHORUS:

View File

@ -59,6 +59,9 @@ class SongsTab(SettingsTab):
self.add_from_service_check_box = QtGui.QCheckBox(self.mode_group_box)
self.add_from_service_check_box.setObjectName('add_from_service_check_box')
self.mode_layout.addWidget(self.add_from_service_check_box)
self.display_songbook_check_box = QtGui.QCheckBox(self.mode_group_box)
self.display_songbook_check_box.setObjectName('songbook_check_box')
self.mode_layout.addWidget(self.display_songbook_check_box)
self.left_layout.addWidget(self.mode_group_box)
self.left_layout.addStretch()
self.right_layout.addStretch()
@ -66,6 +69,7 @@ class SongsTab(SettingsTab):
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
self.display_songbook_check_box.stateChanged.connect(self.on_songbook_check_box_changed)
def retranslateUi(self):
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode'))
@ -75,6 +79,7 @@ class SongsTab(SettingsTab):
self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
self.add_from_service_check_box.setText(translate('SongsPlugin.SongsTab',
'Import missing songs from service files'))
self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
def on_search_as_type_check_box_changed(self, check_state):
self.song_search = (check_state == QtCore.Qt.Checked)
@ -88,6 +93,9 @@ class SongsTab(SettingsTab):
def on_add_from_service_check_box_changed(self, check_state):
self.update_load = (check_state == QtCore.Qt.Checked)
def on_songbook_check_box_changed(self, check_state):
self.display_songbook = (check_state == QtCore.Qt.Checked)
def load(self):
settings = Settings()
settings.beginGroup(self.settings_section)
@ -95,10 +103,12 @@ class SongsTab(SettingsTab):
self.tool_bar = settings.value('display songbar')
self.update_edit = settings.value('update service on edit')
self.update_load = settings.value('add song from service')
self.display_songbook = settings.value('display songbook')
self.search_as_type_check_box.setChecked(self.song_search)
self.tool_bar_active_check_box.setChecked(self.tool_bar)
self.update_on_edit_check_box.setChecked(self.update_edit)
self.add_from_service_check_box.setChecked(self.update_load)
self.display_songbook_check_box.setChecked(self.display_songbook)
settings.endGroup()
def save(self):
@ -108,6 +118,7 @@ class SongsTab(SettingsTab):
settings.setValue('display songbar', self.tool_bar)
settings.setValue('update service on edit', self.update_edit)
settings.setValue('add song from service', self.update_load)
settings.setValue('display songbook', self.display_songbook)
settings.endGroup()
if self.tab_visited:
self.settings_form.register_post_process('songs_config_updated')

View File

@ -68,7 +68,7 @@ class SundayPlusImport(SongImport):
for filename in self.import_source:
if self.stop_import_flag:
return
song_file = open(filename)
song_file = open(filename, 'rb')
self.do_import_file(song_file)
song_file.close()
@ -103,7 +103,7 @@ class SundayPlusImport(SongImport):
# Now we are looking for the name.
if data[i:i + 1] == '#':
name_end = data.find(':', i + 1)
name = data[i + 1:name_end]
name = data[i + 1:name_end].upper()
i = name_end + 1
while data[i:i + 1] == ' ':
i += 1
@ -129,13 +129,13 @@ class SundayPlusImport(SongImport):
value = data[i:end]
# If we are in the main group.
if not cell:
if name == 'title':
if name == 'TITLE':
self.title = self.decode(self.unescape(value))
elif name == 'Author':
elif name == 'AUTHOR':
author = self.decode(self.unescape(value))
if len(author):
self.add_author(author)
elif name == 'Copyright':
elif name == 'COPYRIGHT':
self.copyright = self.decode(self.unescape(value))
elif name[0:4] == 'CELL':
self.parse(value, cell=name[4:])
@ -147,12 +147,12 @@ class SundayPlusImport(SongImport):
verse_type = VerseType.tags[VerseType.from_loose_input(value[0])]
if len(value) >= 2 and value[-1] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
verse_type = "%s%s" % (verse_type, value[-1])
elif name == 'Hotkey':
# Hotkey always appears after MARKER_NAME, so it
elif name == 'HOTKEY':
# HOTKEY always appears after MARKER_NAME, so it
# effectively overrides MARKER_NAME, if present.
if len(value) and value in list(HOTKEY_TO_VERSE_TYPE.keys()):
verse_type = HOTKEY_TO_VERSE_TYPE[value]
if name == 'rtf':
if name == 'RTF':
value = self.unescape(value)
result = strip_rtf(value, self.encoding)
if result is None:

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
@ -302,9 +310,9 @@ class OpenLyrics(object):
verse_tag = verse[0]['type'][0].lower()
verse_number = verse[0]['label']
verse_def = verse_tag + verse_number
verse_tags.append(verse_def)
# Create the letter from the number of duplicates
verse[0]['suffix'] = chr(96 + verse_tags.count(verse_def))
verse[0][u'suffix'] = chr(97 + (verse_tags.count(verse_def) % 26))
verse_tags.append(verse_def)
# If the verse tag is a duplicate use the suffix letter
for verse in verse_list:
verse_tag = verse[0]['type'][0].lower()
@ -336,7 +344,7 @@ class OpenLyrics(object):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of two unicode strings::
(u'{st}{r}', u'{/r}{/st}')
('{st}{r}', '{/r}{/st}')
The first unicode string are the start tags (for the next slide). The second unicode string are the end tags.
@ -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):
"""

View File

@ -63,6 +63,7 @@ __default_settings__ = {
'songs/search as type': False,
'songs/add song from service': True,
'songs/display songbar': True,
'songs/display songbook': False,
'songs/last directory import': '',
'songs/last directory export': '',
'songs/songselect username': '',

View File

@ -30,6 +30,7 @@
from PyQt4 import QtCore, QtGui
from openlp.core.common import translate
from openlp.core.lib import build_icon
from openlp.core.lib.ui import create_button_box
@ -44,6 +45,7 @@ class Ui_SongUsageDeleteDialog(object):
:param song_usage_delete_dialog:
"""
song_usage_delete_dialog.setObjectName('song_usage_delete_dialog')
song_usage_delete_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_usage_delete_dialog.resize(291, 243)
self.vertical_layout = QtGui.QVBoxLayout(song_usage_delete_dialog)
self.vertical_layout.setSpacing(8)

View File

@ -45,6 +45,7 @@ class Ui_SongUsageDetailDialog(object):
:param song_usage_detail_dialog:
"""
song_usage_detail_dialog.setObjectName('song_usage_detail_dialog')
song_usage_detail_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
song_usage_detail_dialog.resize(609, 413)
self.vertical_layout = QtGui.QVBoxLayout(song_usage_detail_dialog)
self.vertical_layout.setSpacing(8)

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

@ -150,6 +150,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

@ -148,7 +148,7 @@ class JenkinsTrigger(object):
def get_repo_name():
"""
This returns the name of branch of the wokring directory. For example it returns *lp:~googol/openlp/render*.
This returns the name of branch of the working directory. For example it returns *lp:~googol/openlp/render*.
"""
# Run the bzr command.
bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
@ -198,7 +198,7 @@ def main():
jenkins_trigger = JenkinsTrigger(token)
try:
jenkins_trigger.trigger_build()
except HTTPError as e:
except HTTPError:
print('Wrong token.')
return
# Open the browser before printing the output.

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 [

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