Merge trunk

This commit is contained in:
Samuel Mehrbrodt 2015-12-11 09:33:24 +01:00
commit 0d4541b8dc
125 changed files with 13430 additions and 11728 deletions

View File

@ -1 +1 @@
2.1.6
2.2

View File

@ -30,7 +30,7 @@ logging and a plugin framework are contained within the openlp.core module.
import os
import sys
import logging
from optparse import OptionParser
import argparse
from traceback import format_exception
import shutil
import time
@ -282,17 +282,18 @@ def parse_options(args):
:return: a tuple of parsed options of type optparse.Value and a list of remaining argsZ
"""
# Set up command line options.
usage = 'Usage: %prog [options] [qt-options]'
parser = OptionParser(usage=usage)
parser.add_option('-e', '--no-error-form', dest='no_error_form', action='store_true',
help='Disable the error notification form.')
parser.add_option('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
parser.add_option('-p', '--portable', dest='portable', action='store_true',
help='Specify if this should be run as a portable app, off a USB flash drive (not implemented).')
parser.add_option('-d', '--dev-version', dest='dev_version', action='store_true',
help='Ignore the version file and pull the version directly from Bazaar')
parser.add_option('-s', '--style', dest='style', help='Set the Qt4 style (passed directly to Qt4).')
parser = argparse.ArgumentParser(prog='openlp.py')
parser.add_argument('-e', '--no-error-form', dest='no_error_form', action='store_true',
help='Disable the error notification form.')
parser.add_argument('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
parser.add_argument('-p', '--portable', dest='portable', action='store_true',
help='Specify if this should be run as a portable app, '
'off a USB flash drive (not implemented).')
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
help='Ignore the version file and pull the version directly from Bazaar')
parser.add_argument('-s', '--style', dest='style', help='Set the Qt4 style (passed directly to Qt4).')
parser.add_argument('rargs', nargs='?', default=[])
# Parse command line options and deal with them. Use args supplied pragmatically if possible.
return parser.parse_args(args) if args else parser.parse_args()
@ -318,18 +319,18 @@ def main(args=None):
:param args: Some args
"""
(options, args) = parse_options(args)
args = parse_options(args)
qt_args = []
if options.loglevel.lower() in ['d', 'debug']:
if args and args.loglevel.lower() in ['d', 'debug']:
log.setLevel(logging.DEBUG)
elif options.loglevel.lower() in ['w', 'warning']:
elif args and args.loglevel.lower() in ['w', 'warning']:
log.setLevel(logging.WARNING)
else:
log.setLevel(logging.INFO)
if options.style:
qt_args.extend(['-style', options.style])
if args and args.style:
qt_args.extend(['-style', args.style])
# Throw the rest of the arguments at Qt, just in case.
qt_args.extend(args)
qt_args.extend(args.rargs)
# Bug #1018855: Set the WM_CLASS property in X11
if not is_win() and not is_macosx():
qt_args.append('OpenLP')
@ -339,7 +340,7 @@ def main(args=None):
application = OpenLP(qt_args)
application.setOrganizationName('OpenLP')
application.setOrganizationDomain('openlp.org')
if options.portable:
if args and args.portable:
application.setApplicationName('OpenLPPortable')
Settings.setDefaultFormat(Settings.IniFormat)
# Get location OpenLPPortable.ini
@ -383,6 +384,6 @@ def main(args=None):
application.installTranslator(default_translator)
else:
log.debug('Could not find default_translator.')
if not options.no_error_form:
if args and not args.no_error_form:
sys.excepthook = application.hook_exception
sys.exit(application.run(qt_args))

View File

@ -99,7 +99,7 @@ class AppLocation(object):
Get a list of files from the data files path.
:param section: Defaults to *None*. The section of code getting the files - used to load from a section's
data subdirectory.
data subdirectory.
:param extension:
Defaults to *None*. The extension to search for. For example::

View File

@ -65,9 +65,9 @@ class Settings(QtCore.QSettings):
* Exposes all the methods of QSettings.
* Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
``IniFormat``, and the path to the Ini file is set using ``set_filename``,
then the Settings constructor (without any arguments) will create a Settings
object for accessing settings stored in that Ini file.
``IniFormat``, and the path to the Ini file is set using ``set_filename``,
then the Settings constructor (without any arguments) will create a Settings
object for accessing settings stored in that Ini file.
``__default_settings__``
This dict contains all core settings with their default values.
@ -118,6 +118,7 @@ class Settings(QtCore.QSettings):
'advanced/slide limits': SlideLimits.End,
'advanced/single click preview': False,
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
'advanced/search as type': True,
'crashreport/last directory': '',
'formattingTags/html_tags': '',
'core/audio repeat list': False,
@ -321,48 +322,10 @@ class Settings(QtCore.QSettings):
}
__file_path__ = ''
__obsolete_settings__ = [
# Changed during 1.9.x development.
('bibles/bookname language', 'bibles/book name language', []),
('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]),
('songs/ccli number', 'core/ccli number', []),
('media/use phonon', '', []),
# Changed during 2.1.x development.
('advanced/stylesheet fix', '', []),
('bibles/last directory 1', 'bibles/last directory import', []),
('media/background color', 'players/background color', []),
('themes/last directory', 'themes/last directory import', []),
('themes/last directory 1', 'themes/last directory export', []),
('songs/last directory 1', 'songs/last directory import', []),
('songusage/last directory 1', 'songusage/last directory export', []),
('user interface/mainwindow splitter geometry', 'user interface/main window splitter geometry', []),
('shortcuts/makeLive', 'shortcuts/make_live', []),
('general/audio repeat list', 'core/audio repeat list', []),
('general/auto open', 'core/auto open', []),
('general/auto preview', 'core/auto preview', []),
('general/audio start paused', 'core/audio start paused', []),
('general/auto unblank', 'core/auto unblank', []),
('general/blank warning', 'core/blank warning', []),
('general/ccli number', 'core/ccli number', []),
('general/has run wizard', 'core/has run wizard', []),
('general/language', 'core/language', []),
('general/last version test', 'core/last version test', []),
('general/loop delay', 'core/loop delay', []),
('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
('general/save prompt', 'core/save prompt', []),
('general/screen blank', 'core/screen blank', []),
('general/show splash', 'core/show splash', []),
('general/songselect password', 'core/songselect password', []),
('general/songselect username', 'core/songselect username', []),
('general/update check', 'core/update check', []),
('general/view mode', 'core/view mode', []),
('general/display on monitor', 'core/display on monitor', []),
('general/override position', 'core/override position', []),
('general/x position', 'core/x position', []),
('general/y position', 'core/y position', []),
('general/monitor', 'core/monitor', []),
('general/height', 'core/height', []),
('general/monitor', 'core/monitor', []),
('general/width', 'core/width', [])
# Changed during 2.2.x development.
# ('advanced/stylesheet fix', '', []),
# ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
('songs/search as type', 'advanced/search as type', [])
]
@staticmethod

View File

@ -108,8 +108,9 @@ class UiStrings(object):
self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural')
self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
self.OLPV2 = translate('OpenLP.Ui', 'OpenLP 2')
self.OLPV2x = translate('OpenLP.Ui', 'OpenLP 2.2')
self.OLP = translate('OpenLP.Ui', 'OpenLP')
self.OLPV2 = "%s %s" % (self.OLP, "2")
self.OLPV2x = "%s %s" % (self.OLP, "2.4")
self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
self.OpenService = translate('OpenLP.Ui', 'Open service.')
self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')

View File

@ -83,7 +83,7 @@ def get_text_file_string(text_file):
None.
:param text_file: The name of the file.
:return The file as a single string
:return: The file as a single string
"""
if not os.path.isfile(text_file):
return False
@ -108,7 +108,7 @@ def str_to_bool(string_value):
Convert a string version of a boolean into a real boolean.
:param string_value: The string value to examine and convert to a boolean type.
:return The correct boolean value
:return: The correct boolean value
"""
if isinstance(string_value, bool):
return string_value
@ -123,7 +123,7 @@ def build_icon(icon):
:param icon:
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
:return The build icon.
:return: The build icon.
"""
button_icon = QtGui.QIcon()
if isinstance(icon, QtGui.QIcon):
@ -168,7 +168,7 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
height of 88 is used.
:return The final icon.
:return: The final icon.
"""
ext = os.path.splitext(thumb_path)[1].lower()
reader = QtGui.QImageReader(image_path)
@ -194,7 +194,7 @@ def validate_thumb(file_path, thumb_path):
:param file_path: The path to the file. The file **must** exist!
:param thumb_path: The path to the thumb.
:return True, False if the image has changed since the thumb was created.
:return: True, False if the image has changed since the thumb was created.
"""
if not os.path.exists(thumb_path):
return False

View File

@ -164,29 +164,29 @@ class FormattingTags(object):
Add a list of tags to the list.
:param tags: The list with tags to add.
Each **tag** has to be a ``dict`` and should have the following keys:
Each **tag** has to be a ``dict`` and should have the following keys:
* desc
The formatting tag's description, e. g. **Red**
* desc
The formatting tag's description, e. g. **Red**
* start tag
The start tag, e. g. ``{r}``
* start tag
The start tag, e. g. ``{r}``
* end tag
The end tag, e. g. ``{/r}``
* end tag
The end tag, e. g. ``{/r}``
* start html
The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
* start html
The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
* end html
The end html tag. For example ``</span>``
* end html
The end html tag. For example ``</span>``
* protected
A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
* protected
A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
* temporary
A temporary tag will not be saved, but is also considered when displaying text containing the tag. It has
to be a ``boolean``.
* temporary
A temporary tag will not be saved, but is also considered when displaying text containing the tag. It
has to be a ``boolean``.
"""
FormattingTags.html_expands.extend(tags)

View File

@ -288,13 +288,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
"""
Perform tasks on application startup
"""
# FIXME: Remove after 2.2 release.
# This is needed to load the list of media/presentation from the config saved before the settings rewrite.
if self.media_item_class is not None and self.name != 'images':
loaded_list = Settings().get_files_from_config(self)
# Now save the list to the config using our Settings class.
if loaded_list:
Settings().setValue('%s/%s files' % (self.settings_section, self.name), loaded_list)
pass
def uses_theme(self, theme):
"""

View File

@ -25,7 +25,9 @@
See PJLink Class 1 Specifications for details.
http://pjlink.jbmia.or.jp/english/dl.html
Section 5-1 PJLink Specifications
Section 5-5 Guidelines for Input Terminals
NOTE:

View File

@ -89,9 +89,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
:param theme_name: The current theme name.
:param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
Defaults to *None*.
Defaults to *None*.
:param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
used when a theme is permanently deleted.
used when a theme is permanently deleted.
"""
if old_theme_name is not None and old_theme_name in self._theme_dimensions:
del self._theme_dimensions[old_theme_name]

View File

@ -142,7 +142,9 @@ class ScreenList(object):
"""
Add a screen to the list of known screens.
:param screen: A dict with the screen properties::
:param screen: A dict with the screen properties:
::
{
'primary': True,

View File

@ -114,17 +114,17 @@ class SearchEdit(QtGui.QLineEdit):
default.
:param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
instance or string) and a title for the item in the menu. In short, they should look like this::
instance or string) and a title for the item in the menu. In short, they should look like this::
(<identifier>, <icon>, <title>, <place holder text>)
(<identifier>, <icon>, <title>, <place holder text>)
For instance::
For instance::
(1, <QIcon instance>, "Titles", "Search Song Titles...")
(1, <QIcon instance>, "Titles", "Search Song Titles...")
Or::
Or::
(2, ":/songs/authors.png", "Authors", "Search Authors...")
(2, ":/songs/authors.png", "Authors", "Search Authors...")
"""
menu = QtGui.QMenu(self)
first = None

View File

@ -129,7 +129,7 @@ class ItemCapabilities(object):
OnLoadUpdate = 8
AddIfNewItem = 9
ProvidesOwnDisplay = 10
HasDetailedTitleDisplay = 11
# HasDetailedTitleDisplay = 11
HasVariableStartTime = 12
CanSoftBreak = 13
CanWordSplit = 14
@ -388,7 +388,7 @@ class ServiceItem(RegistryProperties):
:param service_item: The item to extract data from.
:param path: Defaults to *None*. This is the service manager path for things which have their files saved
with them or None when the saved service is lite and the original file paths need to be preserved.
with them or None when the saved service is lite and the original file paths need to be preserved.
"""
log.debug('set_from_service called with path %s' % path)
header = service_item['serviceitem']['header']
@ -415,11 +415,6 @@ class ServiceItem(RegistryProperties):
self.will_auto_start = header.get('will_auto_start', False)
self.processor = header.get('processor', None)
self.has_original_files = True
# TODO: Remove me in 2,3 build phase
if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay):
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
self.processor = self.title
self.title = None
if 'background_audio' in header:
self.background_audio = []
for filename in header['background_audio']:

View File

@ -66,9 +66,9 @@ def create_button_box(dialog, name, standard_buttons, custom_buttons=None):
:param dialog: The parent object. This has to be a ``QDialog`` descendant.
:param name: A string which is set as object name.
:param standard_buttons: A list of strings for the used buttons. It might contain: ``ok``, ``save``, ``cancel``,
``close``, and ``defaults``.
``close``, and ``defaults``.
:param custom_buttons: A list of additional buttons. If an item is an instance of QtGui.QAbstractButton it is added
with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
"""
if custom_buttons is None:
custom_buttons = []

View File

@ -154,114 +154,147 @@ class UiAboutDialog(object):
'zh_CN': [' "executor" ']
}
documentors = ['Wesley "wrst" Stout', 'John "jseagull1" Cegalis (lead)']
project_lead = translate('OpenLP.AboutForm', 'Project Lead')
devs = translate('OpenLP.AboutForm', 'Developers')
cons = translate('OpenLP.AboutForm', 'Contributors')
packs = translate('OpenLP.AboutForm', 'Packagers')
tests = translate('OpenLP.AboutForm', 'Testers')
laters = translate('OpenLP.AboutForm', 'Translators')
af = translate('OpenLP.AboutForm', 'Afrikaans (af)')
cs = translate('OpenLP.AboutForm', 'Czech (cs)')
da = translate('OpenLP.AboutForm', 'Danish (da)')
de = translate('OpenLP.AboutForm', 'German (de)')
el = translate('OpenLP.AboutForm', 'Greek (el)')
gb = translate('OpenLP.AboutForm', 'English, United Kingdom (en_GB)')
enza = translate('OpenLP.AboutForm', 'English, South Africa (en_ZA)')
es = translate('OpenLP.AboutForm', 'Spanish (es)')
et = translate('OpenLP.AboutForm', 'Estonian (et)')
fi = translate('OpenLP.AboutForm', 'Finnish (fi)')
fr = translate('OpenLP.AboutForm', 'French (fr)')
hu = translate('OpenLP.AboutForm', 'Hungarian (hu)')
ind = translate('OpenLP.AboutForm', 'Indonesian (id)')
ja = translate('OpenLP.AboutForm', 'Japanese (ja)')
nb = translate('OpenLP.AboutForm', 'Norwegian Bokm\xe5l (nb)')
nl = translate('OpenLP.AboutForm', 'Dutch (nl)')
pl = translate('OpenLP.AboutForm', 'Polish (pl)')
ptbr = translate('OpenLP.AboutForm', 'Portuguese, Brazil (pt_BR)')
ru = translate('OpenLP.AboutForm', 'Russian (ru)')
sv = translate('OpenLP.AboutForm', 'Swedish (sv)')
talk = translate('OpenLP.AboutForm', 'Tamil(Sri-Lanka) (ta_LK)')
zhcn = translate('OpenLP.AboutForm', 'Chinese(China) (zh_CN)')
documentation = translate('OpenLP.AboutForm', 'Documentation')
built_with = translate('OpenLP.AboutForm', 'Built With\n'
' Python: http://www.python.org/\n'
' Qt4: http://qt.io\n'
' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro\n'
' Oxygen Icons: http://techbase.kde.org/Projects/Oxygen/\n'
' MuPDF: http://www.mupdf.com/\n')
final_credit = translate('OpenLP.AboutForm', 'Final Credit\n'
' "For God so loved the world that He gave\n'
' His one and only Son, so that whoever\n'
' believes in Him will not perish but inherit\n'
' eternal life." -- John 3:16\n\n'
' And last but not least, final credit goes to\n'
' God our Father, for sending His Son to die\n'
' on the cross, setting us free from sin. We\n'
' bring this software to you for free because\n'
' He has set us free.')
self.credits_text_edit.setPlainText(
translate('OpenLP.AboutForm',
'Project Lead\n'
' %s\n'
'\n'
'Developers\n'
' %s\n'
'\n'
'Contributors\n'
' %s\n'
'\n'
'Testers\n'
' %s\n'
'\n'
'Packagers\n'
' %s\n'
'\n'
'Translators\n'
' Afrikaans (af)\n'
' %s\n'
' Czech (cs)\n'
' %s\n'
' Danish (da)\n'
' %s\n'
' German (de)\n'
' %s\n'
' Greek (el)\n'
' %s\n'
' English, United Kingdom (en_GB)\n'
' %s\n'
' English, South Africa (en_ZA)\n'
' %s\n'
' Spanish (es)\n'
' %s\n'
' Estonian (et)\n'
' %s\n'
' Finnish (fi)\n'
' %s\n'
' French (fr)\n'
' %s\n'
' Hungarian (hu)\n'
' %s\n'
' Indonesian (id)\n'
' %s\n'
' Japanese (ja)\n'
' %s\n'
' Norwegian Bokm\xe5l (nb)\n'
' %s\n'
' Dutch (nl)\n'
' %s\n'
' Polish (pl)\n'
' %s\n'
' Portuguese, Brazil (pt_BR)\n'
' %s\n'
' Russian (ru)\n'
' %s\n'
' Swedish (sv)\n'
' %s\n'
' Tamil(Sri-Lanka) (ta_LK)\n'
' %s\n'
' Chinese(China) (zh_CN)\n'
' %s\n'
'\n'
'Documentation\n'
' %s\n'
'\n'
'Built With\n'
' Python: http://www.python.org/\n'
' Qt4: http://qt.io\n'
' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro\n'
' Oxygen Icons: http://techbase.kde.org/Projects/Oxygen/\n'
' MuPDF: http://www.mupdf.com/\n'
'\n'
'Final Credit\n'
' "For God so loved the world that He gave\n'
' His one and only Son, so that whoever\n'
' believes in Him will not perish but inherit\n'
' eternal life." -- John 3:16\n\n'
' And last but not least, final credit goes to\n'
' God our Father, for sending His Son to die\n'
' on the cross, setting us free from sin. We\n'
' bring this software to you for free because\n'
' He has set us free.') %
(lead, '\n '.join(developers),
'\n '.join(contributors), '\n '.join(testers),
'\n '.join(packagers), '\n '.join(translators['af']),
'\n '.join(translators['cs']),
'\n '.join(translators['da']),
'\n '.join(translators['de']),
'\n '.join(translators['el']),
'\n '.join(translators['en_GB']),
'\n '.join(translators['en_ZA']),
'\n '.join(translators['es']),
'\n '.join(translators['et']),
'\n '.join(translators['fi']),
'\n '.join(translators['fr']),
'\n '.join(translators['hu']),
'\n '.join(translators['id']),
'\n '.join(translators['ja']),
'\n '.join(translators['nb']),
'\n '.join(translators['nl']),
'\n '.join(translators['pl']),
'\n '.join(translators['pt_BR']),
'\n '.join(translators['ru']),
'\n '.join(translators['sv']),
'\n '.join(translators['ta_LK']),
'\n '.join(translators['zh_CN']),
'\n '.join(documentors)))
'%s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
' %s\n'
'\n'
'%s\n'
' %s\n'
'\n'
'%s\n%s' %
(project_lead, lead,
devs, '\n '.join(developers),
cons, '\n '.join(contributors),
tests, '\n '.join(testers),
packs, '\n '.join(packagers),
laters,
af, '\n '.join(translators['af']),
cs, '\n '.join(translators['cs']),
da, '\n '.join(translators['da']),
de, '\n '.join(translators['de']),
el, '\n '.join(translators['el']),
gb, '\n '.join(translators['en_GB']),
enza, '\n '.join(translators['en_ZA']),
es, '\n '.join(translators['es']),
et, '\n '.join(translators['et']),
fi, '\n '.join(translators['fi']),
fr, '\n '.join(translators['fr']),
hu, '\n '.join(translators['hu']),
ind, '\n '.join(translators['id']),
ja, '\n '.join(translators['ja']),
nb, '\n '.join(translators['nb']),
nl, '\n '.join(translators['nl']),
pl, '\n '.join(translators['pl']),
ptbr, '\n '.join(translators['pt_BR']),
ru, '\n '.join(translators['ru']),
sv, '\n '.join(translators['sv']),
talk, '\n '.join(translators['ta_LK']),
zhcn, '\n '.join(translators['zh_CN']),
documentation, '\n '.join(documentors),
built_with, final_credit))
self.about_notebook.setTabText(self.about_notebook.indexOf(self.credits_tab),
translate('OpenLP.AboutForm', 'Credits'))
copyright_note = translate('OpenLP.AboutForm',

View File

@ -80,6 +80,9 @@ class AdvancedTab(SettingsTab):
self.expand_service_item_check_box = QtGui.QCheckBox(self.ui_group_box)
self.expand_service_item_check_box.setObjectName('expand_service_item_check_box')
self.ui_layout.addRow(self.expand_service_item_check_box)
self.search_as_type_check_box = QtGui.QCheckBox(self.ui_group_box)
self.search_as_type_check_box.setObjectName('SearchAsType_check_box')
self.ui_layout.addRow(self.search_as_type_check_box)
self.enable_auto_close_check_box = QtGui.QCheckBox(self.ui_group_box)
self.enable_auto_close_check_box.setObjectName('enable_auto_close_check_box')
self.ui_layout.addRow(self.enable_auto_close_check_box)
@ -251,6 +254,7 @@ class AdvancedTab(SettingsTab):
self.end_slide_radio_button.clicked.connect(self.on_end_slide_button_clicked)
self.wrap_slide_radio_button.clicked.connect(self.on_wrap_slide_button_clicked)
self.next_item_radio_button.clicked.connect(self.on_next_item_button_clicked)
self.search_as_type_check_box.stateChanged.connect(self.on_search_as_type_check_box_changed)
def retranslateUi(self):
"""
@ -319,6 +323,7 @@ class AdvancedTab(SettingsTab):
self.end_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Remain on Slide'))
self.wrap_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Wrap around'))
self.next_item_radio_button.setText(translate('OpenLP.GeneralTab', '&Move to next/previous service item'))
self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
def load(self):
"""
@ -349,6 +354,8 @@ class AdvancedTab(SettingsTab):
self.default_color = settings.value('default color')
self.default_file_edit.setText(settings.value('default image'))
self.slide_limits = settings.value('slide limits')
self.is_search_as_you_type_enabled = settings.value('search as type')
self.search_as_type_check_box.setChecked(self.is_search_as_you_type_enabled)
# Prevent the dialog displayed by the alternate_rows_check_box to display.
self.alternate_rows_check_box.blockSignals(True)
self.alternate_rows_check_box.setChecked(settings.value('alternate rows'))
@ -424,8 +431,14 @@ class AdvancedTab(SettingsTab):
settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked())
self.settings_form.register_post_process('config_screen_changed')
self.settings_form.register_post_process('slidecontroller_update_slide_limits')
settings.setValue('search as type', self.is_search_as_you_type_enabled)
settings.endGroup()
def on_search_as_type_check_box_changed(self, check_state):
self.is_search_as_you_type_enabled = (check_state == QtCore.Qt.Checked)
self.settings_form.register_post_process('songs_config_updated')
self.settings_form.register_post_process('custom_config_updated')
def cancel(self):
"""
Dialogue was cancelled, remove any pending data path change.

View File

@ -173,6 +173,19 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
Registry().register_function('live_display_hide', self.hide_display)
Registry().register_function('live_display_show', self.show_display)
Registry().register_function('update_display_css', self.css_changed)
self.close_display = False
def closeEvent(self, event):
"""
Catch the close event, and check that the close event is triggered by OpenLP closing the display.
On Windows this event can be triggered by pressing ALT+F4, which we want to ignore.
:param event: The triggered event
"""
if self.close_display:
super().closeEvent(event)
else:
event.ignore()
def close(self):
"""
@ -182,6 +195,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
Registry().remove_function('live_display_hide', self.hide_display)
Registry().remove_function('live_display_show', self.show_display)
Registry().remove_function('update_display_css', self.css_changed)
self.close_display = True
super().close()
def set_transparency(self, enabled):
@ -320,7 +334,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
cache.
:param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
If the image has changed it has to be re-added to the image manager.
If the image has changed it has to be re-added to the image manager.
"""
image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
self.controller.media_controller.media_reset(self.controller)

View File

@ -312,6 +312,13 @@ class Ui_MainWindow(object):
icon=':/system/system_help_contents.png',
can_shortcuts=True,
category=UiStrings().Help, triggers=self.on_offline_help_clicked)
elif is_macosx():
self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir),
'..', 'Resources', 'OpenLP.help')
self.offline_help_item = create_action(main_window, 'offlineHelpItem',
icon=':/system/system_help_contents.png',
can_shortcuts=True,
category=UiStrings().Help, triggers=self.on_offline_help_clicked)
self.on_line_help_item = create_action(main_window, 'onlineHelpItem',
icon=':/system/system_online_help.png',
can_shortcuts=True,
@ -354,7 +361,7 @@ class Ui_MainWindow(object):
add_actions(self.tools_menu, (self.tools_open_data_folder, None))
add_actions(self.tools_menu, (self.tools_first_time_wizard, None))
add_actions(self.tools_menu, [self.update_theme_images])
if is_win():
if (is_win() or is_macosx()) and (hasattr(sys, 'frozen') and sys.frozen == 1):
add_actions(self.help_menu, (self.offline_help_item, self.on_line_help_item, None, self.web_site_item,
self.about_item))
else:
@ -382,7 +389,7 @@ class Ui_MainWindow(object):
self.file_menu.setTitle(translate('OpenLP.MainWindow', '&File'))
self.file_import_menu.setTitle(translate('OpenLP.MainWindow', '&Import'))
self.file_export_menu.setTitle(translate('OpenLP.MainWindow', '&Export'))
self.recent_files_menu.setTitle(translate('OpenLP.MainWindow', '&Recent Files'))
self.recent_files_menu.setTitle(translate('OpenLP.MainWindow', '&Recent Services'))
self.view_menu.setTitle(translate('OpenLP.MainWindow', '&View'))
self.view_mode_menu.setTitle(translate('OpenLP.MainWindow', 'M&ode'))
self.tools_menu.setTitle(translate('OpenLP.MainWindow', '&Tools'))
@ -393,16 +400,16 @@ class Ui_MainWindow(object):
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager'))
self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Manager'))
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New Service'))
self.file_new_item.setToolTip(UiStrings().NewService)
self.file_new_item.setStatusTip(UiStrings().CreateService)
self.file_open_item.setText(translate('OpenLP.MainWindow', '&Open'))
self.file_open_item.setText(translate('OpenLP.MainWindow', '&Open Service'))
self.file_open_item.setToolTip(UiStrings().OpenService)
self.file_open_item.setStatusTip(translate('OpenLP.MainWindow', 'Open an existing service.'))
self.file_save_item.setText(translate('OpenLP.MainWindow', '&Save'))
self.file_save_item.setText(translate('OpenLP.MainWindow', '&Save Service'))
self.file_save_item.setToolTip(UiStrings().SaveService)
self.file_save_item.setStatusTip(translate('OpenLP.MainWindow', 'Save the current service to disk.'))
self.file_save_as_item.setText(translate('OpenLP.MainWindow', 'Save &As...'))
self.file_save_as_item.setText(translate('OpenLP.MainWindow', 'Save Service &As...'))
self.file_save_as_item.setToolTip(translate('OpenLP.MainWindow', 'Save Service As'))
self.file_save_as_item.setStatusTip(translate('OpenLP.MainWindow',
'Save the current service under a new name.'))
@ -449,11 +456,11 @@ class Ui_MainWindow(object):
self.lock_panel.setText(translate('OpenLP.MainWindow', 'L&ock Panels'))
self.lock_panel.setStatusTip(translate('OpenLP.MainWindow', 'Prevent the panels being moved.'))
self.view_live_panel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the live panel.'))
self.settings_plugin_list_item.setText(translate('OpenLP.MainWindow', '&Plugin List'))
self.settings_plugin_list_item.setText(translate('OpenLP.MainWindow', '&Manage Plugins'))
self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'List the Plugins'))
self.about_item.setText(translate('OpenLP.MainWindow', '&About'))
self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP'))
if is_win():
if is_win() or is_macosx():
self.offline_help_item.setText(translate('OpenLP.MainWindow', '&User Guide'))
self.on_line_help_item.setText(translate('OpenLP.MainWindow', '&Online Help'))
self.search_shortcut_action.setText(UiStrings().Search)
@ -498,7 +505,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
super(MainWindow, self).__init__()
Registry().register('main_window', self)
self.clipboard = self.application.clipboard()
self.arguments = self.application.args
self.arguments = ''.join(self.application.args)
# Set up settings sections for the main application (not for use by plugins).
self.ui_settings_section = 'user interface'
self.general_settings_section = 'core'
@ -627,7 +634,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
self.live_controller.display.setFocus()
self.activateWindow()
if self.arguments:
self.open_cmd_line_files()
self.open_cmd_line_files(self.arguments)
elif Settings().value(self.general_settings_section + '/auto open'):
self.service_manager_contents.load_last_file()
view_mode = Settings().value('%s/view mode' % self.general_settings_section)
@ -762,7 +769,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
"""
Load the local OpenLP help file
"""
os.startfile(self.local_help_file)
QtGui.QDesktopServices.openUrl(QtCore.QUrl("file:///" + self.local_help_file))
def on_online_help_clicked(self):
"""
@ -1409,15 +1416,11 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
settings.remove('advanced/data path')
self.application.set_normal_cursor()
def open_cmd_line_files(self):
def open_cmd_line_files(self, filename):
"""
Open files passed in through command line arguments
"""
args = []
for a in self.arguments:
args.extend([a])
for filename in args:
if not isinstance(filename, str):
filename = str(filename, sys.getfilesystemencoding())
if filename.endswith(('.osz', '.oszl')):
self.service_manager_contents.load_file(filename)
if not isinstance(filename, str):
filename = str(filename, sys.getfilesystemencoding())
if filename.endswith(('.osz', '.oszl')):
self.service_manager_contents.load_file(filename)

View File

@ -514,9 +514,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
:param display: Which display to use
:param service_item: The ServiceItem containing the details to be played.
"""
used_players = get_media_players()[0]
used_players = get_media_players()
default_player = used_players[0]
if service_item.processor and service_item.processor != UiStrings().Automatic:
used_players = [service_item.processor.lower()]
# check to see if the player is usable else use the default one.
if not service_item.processor.lower() in used_players:
used_players = default_player
else:
used_players = [service_item.processor.lower()]
# If no player, we can't play
if not used_players:
return False

View File

@ -60,7 +60,7 @@ if sys.version_info[0] > 2:
"""Translate string or bytes to bytes.
"""
if isinstance(s, str):
return bytes(s, sys.getfilesystemencoding())
return s.encode()
else:
return s

View File

@ -80,10 +80,8 @@ def get_vlc():
if is_win():
if not isinstance(e, WindowsError) and e.winerror != 126:
raise
elif is_macosx():
pass
else:
raise
pass
if is_vlc_available:
try:
VERSION = vlc.libvlc_get_version().decode('UTF-8')
@ -103,10 +101,14 @@ def get_vlc():
# On linux we need to initialise X threads, but not when running tests.
# This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups
# (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs...
if get_vlc() and is_linux() and 'nose' not in sys.argv[0]:
if is_linux() and 'nose' not in sys.argv[0] and get_vlc():
import ctypes
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so')
try:
x11 = ctypes.cdll.LoadLibrary('libX11.so.6')
except OSError:
# If libx11.so.6 was not found, fallback to more generic libx11.so
x11 = ctypes.cdll.LoadLibrary('libX11.so')
x11.XInitThreads()
except:
log.exception('Failed to run XInitThreads(), VLC might not work properly!')

View File

@ -33,21 +33,21 @@ class Ui_PluginViewDialog(object):
"""
The UI of the plugin view dialog
"""
def setupUi(self, pluginViewDialog):
def setupUi(self, plugin_view_dialog):
"""
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)
plugin_view_dialog.setObjectName('plugin_view_dialog')
plugin_view_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
plugin_view_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
self.plugin_layout = QtGui.QVBoxLayout(plugin_view_dialog)
self.plugin_layout.setObjectName('plugin_layout')
self.list_layout = QtGui.QHBoxLayout()
self.list_layout.setObjectName('list_layout')
self.plugin_list_widget = QtGui.QListWidget(pluginViewDialog)
self.plugin_list_widget = QtGui.QListWidget(plugin_view_dialog)
self.plugin_list_widget.setObjectName('plugin_list_widget')
self.list_layout.addWidget(self.plugin_list_widget)
self.plugin_info_group_box = QtGui.QGroupBox(pluginViewDialog)
self.plugin_info_group_box = QtGui.QGroupBox(plugin_view_dialog)
self.plugin_info_group_box.setObjectName('plugin_info_group_box')
self.plugin_info_layout = QtGui.QFormLayout(self.plugin_info_group_box)
self.plugin_info_layout.setObjectName('plugin_info_layout')
@ -70,15 +70,15 @@ class Ui_PluginViewDialog(object):
self.plugin_info_layout.addRow(self.about_label, self.about_text_browser)
self.list_layout.addWidget(self.plugin_info_group_box)
self.plugin_layout.addLayout(self.list_layout)
self.button_box = create_button_box(pluginViewDialog, 'button_box', ['ok'])
self.button_box = create_button_box(plugin_view_dialog, 'button_box', ['ok'])
self.plugin_layout.addWidget(self.button_box)
self.retranslateUi(pluginViewDialog)
self.retranslateUi(plugin_view_dialog)
def retranslateUi(self, pluginViewDialog):
def retranslateUi(self, plugin_view_dialog):
"""
Translate the UI on the fly
"""
pluginViewDialog.setWindowTitle(translate('OpenLP.PluginForm', 'Plugin List'))
plugin_view_dialog.setWindowTitle(translate('OpenLP.PluginForm', 'Manage Plugins'))
self.plugin_info_group_box.setTitle(translate('OpenLP.PluginForm', 'Plugin Details'))
self.version_label.setText('%s:' % UiStrings().Version)
self.about_label.setText('%s:' % UiStrings().About)

View File

@ -162,7 +162,7 @@ class PrintServiceForm(QtGui.QDialog, Ui_PrintServiceDialog, RegistryProperties)
html_data = self._add_element('html')
self._add_element('head', parent=html_data)
self._add_element('title', self.title_line_edit.text(), html_data.head)
css_path = os.path.join(AppLocation.get_data_path(), 'service_print.css')
css_path = os.path.join(AppLocation.get_data_path(), 'serviceprint', 'service_print.css')
custom_css = get_text_file_string(css_path)
if not custom_css:
custom_css = DEFAULT_CSS

View File

@ -473,8 +473,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
return
projector = list_item.data(QtCore.Qt.UserRole)
msg = QtGui.QMessageBox()
msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name))
msg.setInformativeText('Are you sure you want to delete this projector?')
msg.setText(translate('OpenLP.ProjectorManager', 'Delete projector (%s) %s?') % (projector.link.ip,
projector.link.name))
msg.setInformativeText(translate('OpenLP.ProjectorManager', 'Are you sure you want to delete this projector?'))
msg.setStandardButtons(msg.Cancel | msg.Ok)
msg.setDefaultButton(msg.Cancel)
ans = msg.exec_()

View File

@ -44,17 +44,19 @@ def source_group(inputs, source_text):
Return a dictionary where key is source[0] and values are inputs
grouped by source[0].
source_text = dict{"key1": "key1-text",
"key2": "key2-text",
...}
return:
dict{ key1[0]: { "key11": "key11-text",
"key12": "key12-text",
"key13": "key13-text",
... }
key2[0]: {"key21": "key21-text",
"key22": "key22-text",
... }
::
source_text = dict{"key1": "key1-text",
"key2": "key2-text",
...}
return:
dict{key1[0]: {"key11": "key11-text",
"key12": "key12-text",
"key13": "key13-text",
...}
key2[0]: {"key21": "key21-text",
"key22": "key22-text",
...}
:param inputs: List of inputs
:param source_text: Dictionary of {code: text} values to display
@ -81,16 +83,18 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
Create the radio button page for a tab.
Dictionary will be a 1-key entry where key=tab to setup, val=list of inputs.
source_key: {"groupkey1": {"key11": "key11-text",
"key12": "key12-text",
...
},
"groupkey2": {"key21": "key21-text",
"key22": "key22-text",
....
},
...
}
::
source_key: {"groupkey1": {"key11": "key11-text",
"key12": "key12-text",
...
},
"groupkey2": {"key21": "key21-text",
"key22": "key22-text",
...
},
...
}
:param group: Button group widget to add buttons to
:param source_key: Dictionary of sources for radio buttons

View File

@ -480,7 +480,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
"""
Create the initial service array with the base items to be saved.
:return service array
:return: service array
"""
service = []
core = {'lite-service': self._save_lite,

View File

@ -902,7 +902,8 @@ class SlideController(DisplayController, RegistryProperties):
# This avoids the service theme/desktop flashing on screen
# However opening a new item of the same type will automatically
# close the previous, so make sure we don't close the new one.
if old_item.is_command() and not service_item.is_command():
if old_item.is_command() and not service_item.is_command() or \
old_item.is_command() and not old_item.is_media() and service_item.is_media():
Registry().execute('%s_stop' % old_item.name.lower(), [old_item, self.is_live])
if old_item.is_media() and not service_item.is_media():
self.on_media_close()

View File

@ -503,7 +503,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
Returns a theme object from an XML file
:param theme_name: Name of the theme to load from file
:return The theme object.
:return: The theme object.
"""
self.log_debug('get theme data for theme %s' % theme_name)
xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
@ -519,7 +519,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
Display a warning box to the user that a theme already exists
:param theme_name: Name of the theme.
:return Confirm if the theme is to be overwritten.
:return: Confirm if the theme is to be overwritten.
"""
ret = QtGui.QMessageBox.question(self, translate('OpenLP.ThemeManager', 'Theme Already Exists'),
translate('OpenLP.ThemeManager',
@ -607,7 +607,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
Check if theme already exists and displays error message
:param theme_name: Name of the Theme to test
:return True or False if theme exists
:return: True or False if theme exists
"""
theme_dir = os.path.join(self.path, theme_name)
if os.path.exists(theme_dir):
@ -718,7 +718,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
:param theme_xml: The Theme data object.
:param image_path: Where the theme image is stored
:return Theme data.
:return: Theme data.
"""
theme = ThemeXML()
theme.parse(theme_xml)
@ -734,7 +734,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
:param confirm_text: Confirm message text to be displayed.
:param test_plugin: Do we check the plugins for theme usage.
:param confirm: Do we display a confirm box before run checks.
:return True or False depending on the validity.
:return: True or False depending on the validity.
"""
self.global_theme = Settings().value(self.settings_section + '/global theme')
if check_item_selected(self.theme_list_widget, select_text):
@ -755,12 +755,19 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
return False
# check for use in the system else where.
if test_plugin:
plugin_usage = ""
for plugin in self.plugin_manager.plugins:
if plugin.uses_theme(theme):
critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
translate('OpenLP.ThemeManager',
'Theme %s is used in the %s plugin.')
% (theme, plugin.name))
return False
used_count = plugin.uses_theme(theme)
if used_count:
plugin_usage = "%s%s" % (plugin_usage, (translate('OpenLP.ThemeManager',
'%s time(s) by %s') %
(used_count, plugin.name)))
plugin_usage = "%s\n" % plugin_usage
if plugin_usage:
critical_error_message_box(translate('OpenLP.ThemeManager', 'Unable to delete theme'),
translate('OpenLP.ThemeManager', 'Theme is currently used \n\n%s') %
plugin_usage)
return False
return True
return False

View File

@ -404,6 +404,7 @@ def get_web_page(url, header=None, update_openlp=False):
try:
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
log.debug('Downloaded page {}'.format(page.geturl()))
break
except urllib.error.URLError as err:
log.exception('URLError on {}'.format(url))
log.exception('URLError: {}'.format(err.reason))

View File

@ -238,10 +238,10 @@ class ActionList(object):
:param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
:param category: The category this action belongs to. The category has to be a python string. . **Note**,
if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
if they are added, it is possible to avoid assigning shortcuts twice, which is important.
if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
if they are added, it is possible to avoid assigning shortcuts twice, which is important.
:param weight: The weight specifies how important a category is. However, this only has an impact on the order
the categories are displayed.
the categories are displayed.
"""
if category not in self.categories:
self.categories.append(category)

View File

@ -48,7 +48,11 @@ class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperti
:param message: The message text to be displayed
"""
if message:
self.display_alert(message[0])
text = message[0]
# remove line breaks as these crash javascript code on display
while '\n' in text:
text = text.replace('\n', ' ')
self.display_alert(text)
def display_alert(self, text=''):
"""

View File

@ -178,12 +178,14 @@ class BiblePlugin(Plugin):
def uses_theme(self, theme):
"""
Called to find out if the bible plugin is currently using a theme. Returns ``True`` if the theme is being used,
otherwise returns ``False``.
Called to find out if the bible plugin is currently using a theme. Returns ``1`` if the theme is being used,
otherwise returns ``0``.
:param theme: The theme
"""
return str(self.settings_tab.bible_theme) == theme
if str(self.settings_tab.bible_theme) == theme:
return 1
return 0
def rename_theme(self, old_theme, new_theme):
"""

View File

@ -517,17 +517,19 @@ class BibleImportForm(OpenLPWizard):
critical_error_message_box(translate('BiblesPlugin.ImportWizardForm', 'Error during download'),
translate('BiblesPlugin.ImportWizardForm',
'An error occurred while downloading the list of bibles from %s.'))
self.web_bible_list[download_type] = {}
for (bible_name, bible_key, language_code) in bibles:
self.web_bible_list[download_type][bible_name] = (bible_key, language_code)
bibles = None
if bibles:
self.web_bible_list[download_type] = {}
for (bible_name, bible_key, language_code) in bibles:
self.web_bible_list[download_type][bible_name] = (bible_key, language_code)
self.web_progress_bar.setValue(download_type + 1)
# Update combo box if something got into the list
if self.web_bible_list:
self.on_web_source_combo_box_index_changed(0)
self.web_source_combo_box.setEnabled(True)
self.web_translation_combo_box.setEnabled(True)
self.web_update_button.setEnabled(True)
self.web_progress_bar.setVisible(False)
self.web_source_combo_box.setEnabled(True)
self.web_translation_combo_box.setEnabled(True)
self.web_update_button.setEnabled(True)
self.web_progress_bar.setVisible(False)
def register_fields(self):
"""

View File

@ -37,7 +37,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, transl
from openlp.core.lib.db import BaseModel, init_db, Manager
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.utils import clean_filename
from . import upgrade
from openlp.plugins.bibles.lib import upgrade
log = logging.getLogger(__name__)
@ -219,7 +219,7 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
:param book_id: The id of the book being appended.
:param chapter: The chapter number.
:param text_list: A dict of the verses to be inserted. The key is the verse number, and the value is the
verse text.
verse text.
"""
log.debug('BibleDBcreate_chapter("%s", "%s")' % (book_id, chapter))
# Text list has book and chapter as first two elements of the array.
@ -476,16 +476,6 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
self.save_meta('language_id', language_id)
return language_id
def is_old_database(self):
"""
Returns ``True`` if it is a bible database, which has been created prior to 1.9.6.
"""
try:
self.session.query(Book).all()
except:
return True
return False
def dump_bible(self):
"""
Utility debugging method to dump the contents of a bible.

View File

@ -27,7 +27,6 @@ import re
import socket
import urllib.parse
import urllib.error
from html.parser import HTMLParseError
from bs4 import BeautifulSoup, NavigableString, Tag
@ -290,7 +289,7 @@ class BGExtract(RegistryProperties):
page_source = str(page_source, 'cp1251')
try:
soup = BeautifulSoup(page_source)
except HTMLParseError:
except Exception:
log.error('BeautifulSoup could not parse the Bible page.')
send_error_message('parse')
return None
@ -743,7 +742,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
:param reference_url: The URL to obtain the soup from.
:param header: An optional HTTP header to pass to the bible web server.
:param pre_parse_regex: A regular expression to run on the webpage. Allows manipulation of the webpage before
passing to BeautifulSoup for parsing.
passing to BeautifulSoup for parsing.
:param pre_parse_substitute: The text to replace any matches to the regular expression with.
"""
if not reference_url:
@ -762,7 +761,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
try:
soup = BeautifulSoup(page_source)
CLEANER_REGEX.sub('', str(soup))
except HTMLParseError:
except Exception:
log.exception('BeautifulSoup could not parse the bible page.')
if not soup:
send_error_message('parse')

View File

@ -129,11 +129,6 @@ class BibleManager(RegistryProperties):
bible.session.close()
delete_file(os.path.join(self.path, filename))
continue
# Find old database versions.
if bible.is_old_database():
self.old_bible_databases.append([filename, name])
bible.session.close()
continue
log.debug('Bible Name: "%s"', name)
self.db_cache[name] = bible
# Look to see if lazy load bible exists and get create getter.

View File

@ -30,7 +30,8 @@ from openlp.core.lib.searchedit import SearchEdit
from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \
critical_error_message_box, find_and_set_in_combo_box, build_icon
from openlp.core.utils import get_locale_key
from openlp.plugins.bibles.forms import BibleImportForm, EditBibleForm
from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm
from openlp.plugins.bibles.forms.editbibleform import EditBibleForm
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
LanguageSelection, BibleStrings
from openlp.plugins.bibles.lib.db import BiblesResourcesDB

View File

@ -72,11 +72,9 @@ class CustomPlugin(Plugin):
"""
Called to find out if the custom plugin is currently using a theme.
Returns True if the theme is being used, otherwise returns False.
Returns count of the times the theme is used.
"""
if self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme):
return True
return False
return len(self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme))
def rename_theme(self, old_theme, new_theme):
"""

View File

@ -84,7 +84,7 @@ class CustomXMLBuilder(object):
Add a verse to the ``<lyrics>`` tag.
:param verse_type: A string denoting the type of verse. Possible values are "Chorus", "Verse", "Bridge",
and "Custom".
and "Custom".
:param number: An integer denoting the number of the item, for example: verse 1.
:param content: The actual text of the verse to be stored.

View File

@ -85,6 +85,7 @@ class CustomMediaItem(MediaManagerItem):
"""
log.debug('Config loaded')
self.add_custom_from_service = Settings().value(self.settings_section + '/add custom from service')
self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
def retranslateUi(self):
"""
@ -269,11 +270,12 @@ class CustomMediaItem(MediaManagerItem):
:param text: The search text
"""
search_length = 2
if len(text) > search_length:
self.on_search_text_button_clicked()
elif not text:
self.on_clear_text_button_click()
if self.is_search_as_you_type_enabled:
search_length = 2
if len(text) > search_length:
self.on_search_text_button_clicked()
elif not text:
self.on_clear_text_button_click()
def service_load(self, item):
"""

View File

@ -44,7 +44,7 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
:param clear: Set to False if the text input box should not be cleared when showing the dialog (default: True).
:param show_top_level_group: Set to True when "-- Top level group --" should be showed as first item
(default: False).
(default: False).
:param selected_group: The ID of the group that should be selected by default when showing the dialog.
"""
if clear:

View File

@ -67,36 +67,13 @@ class ImagePlugin(Plugin):
'provided by the theme.')
return about_text
def app_startup(self):
"""
Perform tasks on application startup.
"""
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
Plugin.app_startup(self)
# Convert old settings-based image list to the database.
files_from_config = Settings().get_files_from_config(self)
if files_from_config:
for file in files_from_config:
filename = os.path.split(file)[1]
thumb = os.path.join(self.media_item.service_path, filename)
try:
os.remove(thumb)
except:
pass
log.debug('Importing images list from old config: %s' % files_from_config)
self.media_item.save_new_images_list(files_from_config)
def upgrade_settings(self, settings):
"""
Upgrade the settings of this plugin.
:param settings: The Settings object containing the old settings.
"""
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
files_from_config = settings.get_files_from_config(self)
if files_from_config:
log.debug('Importing images list from old config: %s' % files_from_config)
self.media_item.save_new_images_list(files_from_config)
pass
def set_plugin_text_strings(self):
"""

View File

@ -48,24 +48,24 @@ def init_schema(url):
Setup the images database connection and initialise the database schema.
:param url: The database to setup
The images database contains the following tables:
The images database contains the following tables:
* image_groups
* image_filenames
* image_groups
* image_filenames
**image_groups Table**
This table holds the names of the images groups. It has the following columns:
**image_groups Table**
This table holds the names of the images groups. It has the following columns:
* id
* parent_id
* group_name
* id
* parent_id
* group_name
**image_filenames Table**
This table holds the filenames of the images and the group they belong to. It has the following columns:
**image_filenames Table**
This table holds the filenames of the images and the group they belong to. It has the following columns:
* id
* group_id
* filename
* id
* group_id
* filename
"""
session, metadata = init_db(url)

View File

@ -119,14 +119,6 @@ class ImageMediaItem(MediaManagerItem):
icon=':/general/general_edit.png',
triggers=self.on_edit_click)
create_widget_action(self.list_view, separator=True)
if self.has_delete_icon:
create_widget_action(
self.list_view,
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
text=self.plugin.get_string(StringContent.Delete)['title'],
icon=':/general/general_delete.png',
can_shortcuts=True, triggers=self.on_delete_click)
create_widget_action(self.list_view, separator=True)
create_widget_action(
self.list_view,
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Preview.title()),
@ -155,6 +147,14 @@ class ImageMediaItem(MediaManagerItem):
text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
icon=':/general/general_add.png',
triggers=self.on_add_edit_click)
create_widget_action(self.list_view, separator=True)
if self.has_delete_icon:
create_widget_action(
self.list_view,
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
text=self.plugin.get_string(StringContent.Delete)['title'],
icon=':/general/general_delete.png',
can_shortcuts=True, triggers=self.on_delete_click)
self.add_custom_context_actions()
# Create the context menu and add all actions from the list_view.
self.menu = QtGui.QMenu()
@ -285,7 +285,7 @@ class ImageMediaItem(MediaManagerItem):
:param combobox: The QComboBox to add the options to.
:param parent_group_id: The ID of the group that will be added.
:param prefix: A string containing the prefix that will be added in front of the groupname for each level of
the tree.
the tree.
"""
if parent_group_id == 0:
combobox.clear()
@ -333,7 +333,7 @@ class ImageMediaItem(MediaManagerItem):
:param images: A List of Image Filenames objects that will be used to reload the mediamanager list.
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images.
:param open_group: ImageGroups object of the group that must be expanded after reloading the list in the
interface.
interface.
"""
if not initial_load:
self.application.set_busy_cursor()
@ -469,7 +469,7 @@ class ImageMediaItem(MediaManagerItem):
:param images_list: A List of strings containing image filenames
:param group_id: The ID of the group to save the images in
:param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
the new images
the new images
"""
for filename in images_list:
if not isinstance(filename, str):

View File

@ -43,11 +43,6 @@ log = logging.getLogger(__name__)
CLAPPERBOARD = ':/media/slidecontroller_multimedia.png'
OPTICAL = ':/media/media_optical.png'
VIDEO_ICON = build_icon(':/media/media_video.png')
AUDIO_ICON = build_icon(':/media/media_audio.png')
OPTICAL_ICON = build_icon(OPTICAL)
ERROR_ICON = build_icon(':/general/general_delete.png')
class MediaMediaItem(MediaManagerItem, RegistryProperties):
@ -57,10 +52,20 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
log.info('%s MediaMediaItem loaded', __name__)
def __init__(self, parent, plugin):
self.setup()
super(MediaMediaItem, self).__init__(parent, plugin)
def setup(self):
"""
Allow early setup to be mocked.
"""
self.icon_path = 'images/image'
self.background = False
self.automatic = ''
super(MediaMediaItem, self).__init__(parent, plugin)
self.optical_icon = build_icon(':/media/media_optical.png')
self.video_icon = build_icon(':/media/media_video.png')
self.audio_icon = build_icon(':/media/media_audio.png')
self.error_icon = build_icon(':/general/general_delete.png')
def setup_item(self):
"""
@ -130,7 +135,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
optical_button_tooltip = translate('MediaPlugin.MediaItem',
'Load CD/DVD - only supported when VLC is installed and enabled')
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text,
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=self.optical_icon,
text=optical_button_text,
tooltip=optical_button_tooltip,
triggers=self.on_load_optical)
if disable_optical_button_text:
@ -351,14 +357,14 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
# Handle optical based item
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
item_name = QtGui.QListWidgetItem(clip_name)
item_name.setIcon(OPTICAL_ICON)
item_name.setIcon(self.optical_icon)
item_name.setData(QtCore.Qt.UserRole, track)
item_name.setToolTip('%s@%s-%s' % (file_name, format_milliseconds(start), format_milliseconds(end)))
elif not os.path.exists(track):
# File doesn't exist, mark as error.
file_name = os.path.split(str(track))[1]
item_name = QtGui.QListWidgetItem(file_name)
item_name.setIcon(ERROR_ICON)
item_name.setIcon(self.error_icon)
item_name.setData(QtCore.Qt.UserRole, track)
item_name.setToolTip(track)
elif track_info.isFile():
@ -366,9 +372,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
file_name = os.path.split(str(track))[1]
item_name = QtGui.QListWidgetItem(file_name)
if '*.%s' % (file_name.split('.')[-1].lower()) in self.media_controller.audio_extensions_list:
item_name.setIcon(AUDIO_ICON)
item_name.setIcon(self.audio_icon)
else:
item_name.setIcon(VIDEO_ICON)
item_name.setIcon(self.video_icon)
item_name.setData(QtCore.Qt.UserRole, track)
item_name.setToolTip(track)
if item_name:

View File

@ -290,6 +290,13 @@ class MessageListener(object):
log.info('Message Listener loaded')
def __init__(self, media_item):
self._setup(media_item)
def _setup(self, media_item):
"""
Start up code moved out to make mocking easier
:param media_item: The plugin media item handing Presentations
"""
self.controllers = media_item.controllers
self.media_item = media_item
self.preview_handler = Controller(False)
@ -346,6 +353,12 @@ class MessageListener(object):
self.handler = self.media_item.find_controller_by_type(file)
if not self.handler:
return
else:
# the saved handler is not present so need to use one based on file suffix.
if not self.controllers[self.handler].available:
self.handler = self.media_item.find_controller_by_type(file)
if not self.handler:
return
if is_live:
controller = self.live_handler
else:

View File

@ -137,22 +137,6 @@ class PresentationPlugin(Plugin):
self.register_controllers(controller)
return bool(self.controllers)
def app_startup(self):
"""
Perform tasks on application startup.
"""
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
super().app_startup()
files_from_config = Settings().value('presentations/presentations files')
for file in files_from_config:
try:
self.media_item.clean_up_thumbnails(file, True)
except AttributeError:
pass
self.media_item.list_view.clear()
Settings().setValue('presentations/thumbnail_scheme', 'md5')
self.media_item.validate_and_load(files_from_config)
def about(self):
"""
Return information about this plugin.

View File

@ -271,9 +271,15 @@ window.OpenLP = {
if (typeof value[0] !== "number"){
value[0] = OpenLP.escapeString(value[0])
}
var txt = "";
if (value[2].length > 0) {
txt = value[1] + " ( " + value[2] + " )";
} else {
txt = value[1];
}
ul.append($("<li>").append($("<a>").attr("href", "#options")
.attr("data-rel", "dialog").attr("value", value[0])
.click(OpenLP.showOptions).text(value[1])));
.click(OpenLP.showOptions).text(txt)));
});
}
ul.listview("refresh");

View File

@ -150,6 +150,7 @@ class HttpRouter(RegistryProperties):
self.routes = [
('^/$', {'function': self.serve_file, 'secure': False}),
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
('^/(main)$', {'function': self.serve_file, 'secure': False}),
(r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
@ -170,6 +171,7 @@ class HttpRouter(RegistryProperties):
self.settings_section = 'remotes'
self.translate()
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html')
self.config_dir = os.path.join(AppLocation.get_data_path(), 'stages')
def do_post_processor(self):
"""
@ -309,10 +311,13 @@ class HttpRouter(RegistryProperties):
"""
Translate various strings in the mobile app.
"""
remote = translate('RemotePlugin.Mobile', 'Remote')
stage = translate('RemotePlugin.Mobile', 'Stage View')
live = translate('RemotePlugin.Mobile', 'Live View')
self.template_vars = {
'app_title': translate('RemotePlugin.Mobile', '%s Remote' % UiStrings().OLPV2x),
'stage_title': translate('RemotePlugin.Mobile', '%s Stage View' % UiStrings().OLPV2x),
'live_title': translate('RemotePlugin.Mobile', '%s Live View' % UiStrings().OLPV2x),
'app_title': "%s %s" % (UiStrings().OLPV2x, remote),
'stage_title': "%s %s" % (UiStrings().OLPV2x, stage),
'live_title': "%s %s" % (UiStrings().OLPV2x, live),
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
@ -337,24 +342,32 @@ class HttpRouter(RegistryProperties):
'settings': translate('RemotePlugin.Mobile', 'Settings'),
}
def serve_file(self, file_name=None):
def stages(self, url_path, file_name):
"""
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
If subfolders requested return 404, easier for security for the present.
Allow Stage view to be delivered with custom views.
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
where xx is the language, e.g. 'en'
:param url_path: base path of the URL. Not used but passed by caller
:param file_name: file name with path
:return:
"""
log.debug('serve file request %s' % file_name)
if not file_name:
file_name = 'index.html'
elif file_name == 'stage':
file_name = 'stage.html'
elif file_name == 'main':
file_name = 'main.html'
path = os.path.normpath(os.path.join(self.html_dir, file_name))
if not path.startswith(self.html_dir):
parts = file_name.split('/')
if len(parts) == 1:
file_name = os.path.join(parts[0], 'stage.html')
elif len(parts) == 3:
file_name = os.path.join(parts[1], parts[2])
path = os.path.normpath(os.path.join(self.config_dir, file_name))
if not path.startswith(self.config_dir):
return self.do_not_found()
return self._process_file(path)
def _process_file(self, path):
"""
Common file processing code
:param path: path to file to be loaded
:return: web resource to be loaded
"""
content = None
ext, content_type = self.get_content_type(path)
file_handle = None
@ -377,10 +390,32 @@ class HttpRouter(RegistryProperties):
self.end_headers()
return content
def serve_file(self, file_name=None):
"""
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
If subfolders requested return 404, easier for security for the present.
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
where xx is the language, e.g. 'en'
"""
log.debug('serve file request %s' % file_name)
if not file_name:
file_name = 'index.html'
elif file_name == 'stage':
file_name = 'stage.html'
elif file_name == 'main':
file_name = 'main.html'
path = os.path.normpath(os.path.join(self.html_dir, file_name))
if not path.startswith(self.html_dir):
return self.do_not_found()
return self._process_file(path)
def get_content_type(self, file_name):
"""
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
:param file_name: name of file
"""
ext = os.path.splitext(file_name)[1]
content_type = FILE_TYPES.get(ext, 'text/plain')
@ -389,6 +424,10 @@ class HttpRouter(RegistryProperties):
def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
"""
Serve an image file. If not found return 404.
:param file_name: file name to be served
:param dimensions: image size
:param controller_name: controller to be called
"""
log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
supported_controllers = ['presentations', 'images']
@ -493,6 +532,8 @@ class HttpRouter(RegistryProperties):
def controller_text(self, var):
"""
Perform an action on the slide controller.
:param var: variable - not used
"""
log.debug("controller_text var = %s" % var)
current_item = self.live_controller.service_item
@ -626,6 +667,8 @@ class HttpRouter(RegistryProperties):
def go_live(self, plugin_name):
"""
Go live on an item of type ``plugin``.
:param plugin_name: name of plugin
"""
try:
request_id = json.loads(self.request_data)['request']['id']
@ -639,6 +682,8 @@ class HttpRouter(RegistryProperties):
def add_to_service(self, plugin_name):
"""
Add item of type ``plugin_name`` to the end of the service.
:param plugin_name: name of plugin to be called
"""
try:
request_id = json.loads(self.request_data)['request']['id']

View File

@ -167,7 +167,7 @@ class HTTPSServer(HTTPServer):
local_data = AppLocation.get_directory(AppLocation.DataDir)
self.socket = ssl.SSLSocket(
sock=socket.socket(self.address_family, self.socket_type),
ssl_version=ssl.PROTOCOL_TLSv1,
ssl_version=ssl.PROTOCOL_TLSv1_2,
certfile=os.path.join(local_data, 'remotes', 'openlp.crt'),
keyfile=os.path.join(local_data, 'remotes', 'openlp.key'),
server_side=True)

View File

@ -177,8 +177,9 @@ class RemoteTab(SettingsTab):
'Show thumbnails of non-text slides in remote and stage view.'))
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qr_description_label.setText(
translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/'
'apps/details?id=org.openlp.android">download</a> to install the Android app from Google Play.'))
translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="%s">download</a> to install the '
'Android app from Google Play.') %
'https://play.google.com/store/apps/details?id=org.openlp.android2')
self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
self.https_error_label.setText(
translate('RemotePlugin.RemoteTab', 'Could not find an SSL certificate. The HTTPS server will not be '

View File

@ -178,7 +178,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
if invalid_verses:
valid = create_separated_list(verse_names)
if len(invalid_verses) > 1:
msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s".'
msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s". '
'Valid entries are %(valid)s.\nPlease enter the verses separated by spaces.') % \
{'invalid': ', '.join(invalid_verses), 'valid': valid}
else:

View File

@ -228,6 +228,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
: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::
'SongBeamer Files (*.sng)'
"""
if filters:

View File

@ -173,6 +173,7 @@ def init_schema(url):
Setup the songs database connection and initialise the database schema.
:param url: The database to setup
The song database contains the following tables:
* authors

View File

@ -36,7 +36,7 @@ class EasySlidesImport(SongImport):
Import songs exported from EasySlides
The format example is here:
http://wiki.openlp.org/Development:EasySlides_-_Song_Data_Format
http://wiki.openlp.org/Development:EasySlides\_-_Song_Data_Format
"""
def __init__(self, manager, **kwargs):
"""

View File

@ -171,7 +171,7 @@ class OpenOfficeImport(SongImport):
"""
log.debug('create property OpenOffice')
if is_win():
property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
property_object = self.ooo_manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
else:
property_object = PropertyValue()
property_object.Name = name

View File

@ -71,7 +71,7 @@ class PowerSongImport(SongImport):
"""
Checks if source is a PowerSong 1.0 folder:
* is a directory
* contains at least one *.song file
* contains at least one \*.song file
"""
if os.path.isdir(import_source):
for file in os.listdir(import_source):

View File

@ -105,7 +105,7 @@ class SongImport(QtCore.QObject):
This should be called, when a song could not be imported.
:param file_path: This should be the file path if ``self.import_source`` is a list with different files. If it
is not a list, but a single file (for instance a database), then this should be the song's title.
is not a list, but a single file (for instance a database), then this should be the song's title.
:param reason: The reason why the import failed. The string should be as informative as possible.
"""
self.set_defaults()

View File

@ -36,50 +36,49 @@ class SongProImport(SongImport):
**SongPro Song File Format:**
SongPro has the option to export under its File menu
This produces files containing single or multiple songs
The file is text with lines tagged with # followed by an identifier.
This is documented here: http://creationsoftware.com/ImportIdentifiers.php
An example here: http://creationsoftware.com/ExampleImportingManySongs.txt
#A - next line is the Song Author
#B - the lines following until next tagged line are the "Bridge" words
(can be in rtf or plain text) which we map as B1
#C - the lines following until next tagged line are the chorus words
(can be in rtf or plain text)
which we map as C1
#D - the lines following until next tagged line are the "Ending" words
(can be in rtf or plain text) which we map as E1
#E - this song ends here, so we process the song -
and start again at the next line
#G - next line is the Group
#M - next line is the Song Number
#N - next line are Notes
#R - next line is the SongCopyright
#O - next line is the Verse Sequence
#T - next line is the Song Title
#1 - #7 the lines following until next tagged line are the verse x words
(can be in rtf or plain text)
| SongPro has the option to export under its File menu
| This produces files containing single or multiple songs
| The file is text with lines tagged with # followed by an identifier.
| This is documented here: http://creationsoftware.com/ImportIdentifiers.php
| An example here: http://creationsoftware.com/ExampleImportingManySongs.txt
|
| #A - next line is the Song Author
| #B - the lines following until next tagged line are the "Bridge" words
| (can be in rtf or plain text) which we map as B1
| #C - the lines following until next tagged line are the chorus words
| (can be in rtf or plain text)
| which we map as C1
| #D - the lines following until next tagged line are the "Ending" words
| (can be in rtf or plain text) which we map as E1
| #E - this song ends here, so we process the song -
| and start again at the next line
| #G - next line is the Group
| #M - next line is the Song Number
| #N - next line are Notes
| #R - next line is the SongCopyright
| #O - next line is the Verse Sequence
| #T - next line is the Song Title
| #1 - #7 the lines following until next tagged line are the verse x words
| (can be in rtf or plain text)
"""
def __init__(self, manager, **kwargs):
"""
Initialise the SongPro importer.
"""
SongImport.__init__(self, manager, **kwargs)
super(SongProImport, self).__init__(manager, **kwargs)
def do_import(self):
"""
Receive a single file or a list of files to import.
"""
self.encoding = None
with open(self.import_source, 'r') as songs_file:
with open(self.import_source, 'rt') as songs_file:
self.import_wizard.progress_bar.setMaximum(0)
tag = ''
text = ''
for file_line in songs_file:
if self.stop_import_flag:
break
file_line = str(file_line, 'cp1252')
file_text = file_line.rstrip()
if file_text and file_text[0] == '#':
self.process_section(tag, text.rstrip())
@ -87,6 +86,7 @@ class SongProImport(SongImport):
text = ''
else:
text += file_line
self.finish()
def process_section(self, tag, text):
"""

View File

@ -60,22 +60,22 @@ class SongShowPlusImport(SongImport):
* Each piece of data in the song file has some information that precedes it.
* The general format of this data is as follows:
4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
1 Byte, which tells how many bytes follows
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
The next bytes are the actual data.
The next block of data follows on.
| 4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
| 4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
| 1 Byte, which tells how many bytes follows
| 1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
| The next bytes are the actual data.
| The next block of data follows on.
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some cases
a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string length
descriptors. The first string is the name of the verse, the second is the verse content.
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some
cases a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string
length descriptors. The first string is the name of the verse, the second is the verse content.
The file is ended with four null bytes.
The file is ended with four null bytes.
Valid extensions for a SongShow Plus song file are:
Valid extensions for a SongShow Plus song file are:
* .sbsong
* .sbsong
"""
other_count = 0

View File

@ -24,7 +24,7 @@ The :mod:`worshipcenterpro` module provides the functionality for importing
a WorshipCenter Pro database into the OpenLP database.
"""
import logging
import re
import pyodbc
from openlp.core.common import translate
@ -71,8 +71,41 @@ class WorshipCenterProImport(SongImport):
break
self.set_defaults()
self.title = songs[song]['TITLE']
if 'AUTHOR' in songs[song]:
self.parse_author(songs[song]['AUTHOR'])
if 'CCLISONGID' in songs[song]:
self.ccli_number = songs[song]['CCLISONGID']
if 'COMMENTS' in songs[song]:
self.add_comment(songs[song]['COMMENTS'])
if 'COPY' in songs[song]:
self.add_copyright(songs[song]['COPY'])
if 'SUBJECT' in songs[song]:
self.topics.append(songs[song]['SUBJECT'])
lyrics = songs[song]['LYRICS'].strip('&crlf;&crlf;')
for verse in lyrics.split('&crlf;&crlf;'):
verse = verse.replace('&crlf;', '\n')
self.add_verse(verse)
marker_type = 'v'
# Find verse markers if any
marker_start = verse.find('<')
if marker_start > -1:
marker_end = verse.find('>')
marker = verse[marker_start + 1:marker_end]
# Identify the marker type
if 'REFRAIN' in marker or 'CHORUS' in marker:
marker_type = 'c'
elif 'BRIDGE' in marker:
marker_type = 'b'
elif 'PRECHORUS' in marker:
marker_type = 'p'
elif 'END' in marker:
marker_type = 'e'
elif 'INTRO' in marker:
marker_type = 'i'
elif 'TAG' in marker:
marker_type = 'o'
else:
marker_type = 'v'
# Strip tags from text
verse = re.sub('<[^<]+?>', '', verse)
self.add_verse(verse.strip(), marker_type)
self.finish()

View File

@ -75,7 +75,8 @@ class ZionWorxImport(SongImport):
"""
Receive a CSV file (from a ZionWorx database dump) to import.
"""
with open(self.import_source, 'rb') as songs_file:
# Encoding should always be ISO-8859-1
with open(self.import_source, 'rt', encoding='ISO-8859-1') as songs_file:
field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords',
'DefaultStyle']
songs_reader = csv.DictReader(songs_file, field_names)
@ -112,10 +113,10 @@ class ZionWorxImport(SongImport):
if line and not line.isspace():
verse += line + '\n'
elif verse:
self.add_verse(verse)
self.add_verse(verse, 'v')
verse = ''
if verse:
self.add_verse(verse)
self.add_verse(verse, 'v')
title = self.title
if not self.finish():
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index +
@ -123,8 +124,7 @@ class ZionWorxImport(SongImport):
def _decode(self, str):
"""
Decodes CSV input to unicode, stripping all control characters (except new lines).
Strips all control characters (except new lines).
"""
# This encoding choice seems OK. ZionWorx has no option for setting the
# encoding for its songs, so we assume encoding is always the same.
return str(str, 'cp1252').translate(CONTROL_CHARS_MAP)
# ZionWorx has no option for setting the encoding for its songs, so we assume encoding is always the same.
return str.translate(CONTROL_CHARS_MAP)

View File

@ -115,7 +115,7 @@ class SongMediaItem(MediaManagerItem):
Is triggered when the songs config is updated
"""
log.debug('config_updated')
self.search_as_you_type = Settings().value(self.settings_section + '/search as type')
self.is_search_as_you_type_enabled = Settings().value('advanced/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.display_songbook = Settings().value(self.settings_section + '/display songbook')
@ -279,7 +279,7 @@ class SongMediaItem(MediaManagerItem):
If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start
till 7 characters have been entered.
"""
if self.search_as_you_type:
if self.is_search_as_you_type_enabled:
search_length = 1
if self.search_text_edit.current_search_type() == SongSearch.Entire:
search_length = 4
@ -590,4 +590,4 @@ class SongMediaItem(MediaManagerItem):
:param show_error: Is this an error?
"""
search_results = self.search_entire(string)
return [[song.id, song.title] for song in search_results]
return [[song.id, song.title, song.alternate_title] for song in search_results]

View File

@ -91,7 +91,7 @@ class SongXML(object):
Add a verse to the ``<lyrics>`` tag.
:param type: A string denoting the type of verse. Possible values are *v*, *c*, *b*, *p*, *i*, *e* and *o*.
Any other type is **not** allowed, this also includes translated types.
Any other type is **not** allowed, this also includes translated types.
:param number: An integer denoting the number of the item, for example: verse 1.
:param content: The actual text of the verse to be stored.
:param lang: The verse's language code (ISO-639). This is not required, but should be added if available.
@ -113,6 +113,7 @@ class SongXML(object):
Iterates through the verses in the XML and returns a list of verses and their attributes.
:param xml: The XML of the song to be parsed.
The returned list has the following format::
[[{'type': 'v', 'label': '1'}, u"optional slide split 1[---]optional slide split 2"],
@ -120,17 +121,7 @@ class SongXML(object):
"""
self.song_xml = None
verse_list = []
if not xml.startswith('<?xml') and not xml.startswith('<song'):
# This is an old style song, without XML. Let's handle it correctly by iterating through the verses, and
# then recreating the internal xml object as well.
self.song_xml = objectify.fromstring('<song version="1.0" />')
self.lyrics = etree.SubElement(self.song_xml, 'lyrics')
verses = xml.split('\n\n')
for count, verse in enumerate(verses):
verse_list.append([{'type': 'v', 'label': str(count)}, str(verse)])
self.add_verse_to_lyrics('v', str(count), verse)
return verse_list
elif xml.startswith('<?xml'):
if xml.startswith('<?xml'):
xml = xml[38:]
try:
self.song_xml = objectify.fromstring(xml)
@ -371,7 +362,7 @@ class OpenLyrics(object):
:param xml: The XML to parse (unicode).
:param parse_and_temporary_save: Switch to skip processing the whole song and storing the songs in the database
with a temporary flag. Defaults to ``False``.
with a temporary flag. Defaults to ``False``.
"""
# No xml get out of here.
if not xml:

View File

@ -21,20 +21,21 @@
###############################################################################
"""
The :mod:`songcompare` module provides functionality to search for
duplicate songs. It has one single :function:`songs_probably_equal`.
duplicate songs. It has one single :func:`songs_probably_equal`.
The algorithm is based on the diff algorithm.
First a diffset is calculated for two songs.
To compensate for typos all differences that are smaller than a
limit (<max_typo_size) and are surrounded by larger equal blocks
(>min_fragment_size) are removed and the surrounding equal parts are merged.
Finally two conditions can qualify a song tuple to be a duplicate:
1. There is a block of equal content that is at least min_block_size large.
This condition should hit for all larger songs that have a long enough
equal part. Even if only one verse is equal this condition should still hit.
2. Two thirds of the smaller song is contained in the larger song.
This condition should hit if one of the two songs (or both) is small (smaller
than the min_block_size), but most of the song is contained in the other song.
| The algorithm is based on the diff algorithm.
| First a diffset is calculated for two songs.
| To compensate for typos all differences that are smaller than a
limit (<max_typo_size) and are surrounded by larger equal blocks
(>min_fragment_size) are removed and the surrounding equal parts are merged.
| Finally two conditions can qualify a song tuple to be a duplicate:
1. There is a block of equal content that is at least min_block_size large.
This condition should hit for all larger songs that have a long enough
equal part. Even if only one verse is equal this condition should still hit.
2. Two thirds of the smaller song is contained in the larger song.
This condition should hit if one of the two songs (or both) is small (smaller
than the min_block_size), but most of the song is contained in the other song.
"""
import difflib

View File

@ -23,11 +23,12 @@
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
"""
import logging
import sys
from http.cookiejar import CookieJar
from urllib.parse import urlencode
from urllib.request import HTTPCookieProcessor, URLError, build_opener
from html.parser import HTMLParser
from html import unescape
from bs4 import BeautifulSoup, NavigableString
@ -130,8 +131,8 @@ class SongSelectImport(object):
break
for result in search_results:
song = {
'title': self.html_parser.unescape(result.find('h3').string),
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
'title': unescape(result.find('h3').string),
'authors': [unescape(author.string) for author in result.find_all('li')],
'link': BASE_URL + result.find('a')['href']
}
if callback:
@ -167,7 +168,7 @@ class SongSelectImport(object):
if callback:
callback()
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
song['copyright'] = self.html_parser.unescape(song['copyright'])
song['copyright'] = unescape(song['copyright'])
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
song['verses'] = []
verses = lyrics_page.find('section', 'lyrics').find_all('p')
@ -180,9 +181,9 @@ class SongSelectImport(object):
else:
verse['lyrics'] += '\n'
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
song['verses'].append(self.html_parser.unescape(verse))
song['verses'].append(unescape(verse))
for counter, author in enumerate(song['authors']):
song['authors'][counter] = self.html_parser.unescape(author)
song['authors'][counter] = unescape(author)
return song
def save_song(self, song):

View File

@ -41,9 +41,6 @@ class SongsTab(SettingsTab):
self.mode_group_box.setObjectName('mode_group_box')
self.mode_layout = QtGui.QVBoxLayout(self.mode_group_box)
self.mode_layout.setObjectName('mode_layout')
self.search_as_type_check_box = QtGui.QCheckBox(self.mode_group_box)
self.search_as_type_check_box.setObjectName('SearchAsType_check_box')
self.mode_layout.addWidget(self.search_as_type_check_box)
self.tool_bar_active_check_box = QtGui.QCheckBox(self.mode_group_box)
self.tool_bar_active_check_box.setObjectName('tool_bar_active_check_box')
self.mode_layout.addWidget(self.tool_bar_active_check_box)
@ -62,7 +59,6 @@ class SongsTab(SettingsTab):
self.left_layout.addWidget(self.mode_group_box)
self.left_layout.addStretch()
self.right_layout.addStretch()
self.search_as_type_check_box.stateChanged.connect(self.on_search_as_type_check_box_changed)
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)
@ -71,7 +67,6 @@ class SongsTab(SettingsTab):
def retranslateUi(self):
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode'))
self.search_as_type_check_box.setText(translate('SongsPlugin.SongsTab', 'Enable search as you type'))
self.tool_bar_active_check_box.setText(translate('SongsPlugin.SongsTab',
'Display verses on live tool bar'))
self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
@ -79,8 +74,8 @@ class SongsTab(SettingsTab):
'Import missing songs from service files'))
self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
'Display "%s" symbol before copyright info' %
SongStrings.CopyrightSymbol))
'Display "%s" symbol before copyright info') %
SongStrings.CopyrightSymbol)
def on_search_as_type_check_box_changed(self, check_state):
self.song_search = (check_state == QtCore.Qt.Checked)
@ -103,13 +98,11 @@ class SongsTab(SettingsTab):
def load(self):
settings = Settings()
settings.beginGroup(self.settings_section)
self.song_search = settings.value('search as type')
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.display_copyright_symbol = settings.value('display copyright symbol')
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)
@ -120,7 +113,6 @@ class SongsTab(SettingsTab):
def save(self):
settings = Settings()
settings.beginGroup(self.settings_section)
settings.setValue('search as type', self.song_search)
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)

View File

@ -57,7 +57,6 @@ __default_settings__ = {
'songs/last search type': SongSearch.Entire,
'songs/last import type': SongFormat.OpenLyrics,
'songs/update service on edit': False,
'songs/search as type': True,
'songs/add song from service': True,
'songs/display songbar': True,
'songs/display songbook': False,
@ -226,11 +225,9 @@ class SongsPlugin(Plugin):
Called to find out if the song plugin is currently using a theme.
:param theme: The theme to check for usage
:return: True if the theme is being used, otherwise returns False
:return: count of the number of times the theme is used.
"""
if self.manager.get_all_objects(Song, Song.theme_name == theme):
return True
return False
return len(self.manager.get_all_objects(Song, Song.theme_name == theme))
def rename_theme(self, old_theme, new_theme):
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 766 B

151
scripts/lp-merge.py Executable file
View File

@ -0,0 +1,151 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2015 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This script is used by developers to merge branches from launchpad.net into
other branches, which is basically what happens every time a merge proposal
is merged into trunk. This script simplifies the process and helps avoiding
merging to the wrong branch by doing some checks.
For the script to work it is assumed that the developer doing the merging
has a checkout (not branch) of the target branch.
Merge a branch
--------------
Once a branch has been approved for merging, go to the local folder with the
checkout of the target branch, copy the url from the merge proposal on launchpad
and use it like this:
script/lp-merge.py <url>
The url could look like this:
https://code.launchpad.net/~tomasgroth/openlp/doc22update4/+merge/271874
First you'll be asked whether to merge the branch from the url to the local
branch, which will be done if you answer 'y'. The script checks that the current
folder is the right one before merging.
Then you'll be asked whether to commit the changes to the branch. The script
shows the command it will run, including detected linked bugs and author. If you
wish to change it, you can chose to run 'qcommit', an bzr GUI. Note that you'll
have to install qbzr for this to work. If you choose to run qcommit the script
will print the detected bugs + author for easy copying into the GUI.
"""
import subprocess
import re
import sys
import os
import urllib.request
from bs4 import BeautifulSoup
# Check that the argument count is correct
if len(sys.argv) != 2:
print('\n\tUsage: ' + sys.argv[0] + ' <url to approved merge request>\n')
exit()
url = sys.argv[1]
pattern = re.compile('.+?/\+merge/\d+')
match = pattern.match(url)
# Check that the given argument is an url in the right format
if not url.startswith('https://code.launchpad.net/~') or match is None:
print('The url is not valid! It should look like this:\n '
'https://code.launchpad.net/~tomasgroth/openlp/doc22update4/+merge/271874')
page = urllib.request.urlopen(url)
soup = BeautifulSoup(page.read(), 'lxml')
# Find this span tag that contains the branch url
# <span class="branch-url">
span_branch_url = soup.find('span', class_='branch-url')
branch_url = span_branch_url.contents[0]
# Find this tag that describes the branch. We'll use that for commit message
# <meta name="description" content="...">
meta = soup.find('meta', attrs={"name": "description"})
commit_message = meta.get('content')
# Find all tr-tags with this class. Makes it possible to get bug numbers.
# <tr class="bug-branch-summary"
bug_rows = soup.find_all('tr', class_='bug-branch-summary')
bugs = []
for row in bug_rows:
id_attr = row.get('id')
bugs.append(id_attr[8:])
# Find target branch name using the tag below
# <div class="context-publication"><h1>Merge ... into...
div_branches = soup.find('div', class_='context-publication')
branches = div_branches.h1.contents[0]
target_branch = '+branch/' + branches[(branches.find(' into lp:')+9):]
# Check that we are in the right branch
bzr_info_output = subprocess.check_output(['bzr', 'info'])
if target_branch not in bzr_info_output.decode():
print('ERROR: It seems you are not in the right folder...')
exit()
# Find the authors email address. It is hidden in a javascript line like this:
# conf = {"status_value": "Needs review", "source_revid": "tomasgroth@yahoo.dk-20150921204550-gxduegmcmty9rljf",
# "user_can_edit_status": false, ...
script_tag = soup.find('script', attrs={"id": "codereview-script"})
content = script_tag.contents[0]
start_pos = content.find('source_revid') + 16
pattern = re.compile('.*\w-\d\d\d\d\d+')
match = pattern.match(content[start_pos:])
author_email = match.group()[:-15]
# Merge the branch
do_merge = input('Merge ' + branch_url + ' into local branch? (y/N/q): ').lower()
if do_merge == 'y':
subprocess.call(['bzr', 'merge', branch_url])
elif do_merge == 'q':
exit()
# Create commit command
commit_command = ['bzr', 'commit']
for bug in bugs:
commit_command.append('--fixes')
commit_command.append('lp:' + bug)
commit_command.append('-m')
commit_command.append(commit_message)
commit_command.append('--author')
commit_command.append('"' + author_email + '"')
print('About to run the bzr command below:\n')
print(' '.join(commit_command))
do_commit = input('Run the command (y), use qcommit (qcommit) or cancel (C): ').lower()
if do_commit == 'y':
subprocess.call(commit_command)
elif do_commit == 'qcommit':
# Setup QT workaround to make qbzr look right on my box
my_env = os.environ.copy()
my_env['QT_GRAPHICSSYSTEM'] = 'native'
# Print stuff that kan be copy/pasted into qbzr GUI
print('These bugs can be copy/pasted in: lp:' + ' lp:'.join(bugs))
print('The authors email is: ' + author_email)
# Run qcommit
subprocess.call(['bzr', 'qcommit', '-m', commit_message], env=my_env)

View File

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2015 OpenLP Developers #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import sys
from unittest import TestCase
from openlp.core import parse_options
from tests.helpers.testmixin import TestMixin
class TestInitFunctions(TestMixin, TestCase):
def parse_options_basic_test(self):
"""
Test the parse options process works
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = []
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, [], 'The service file should be blank')
def parse_options_debug_test(self):
"""
Test the parse options process works for debug only
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, [], 'The service file should be blank')
def parse_options_debug_and_portable_test(self):
"""
Test the parse options process works for debug and portable
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['--portable']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertTrue(args.portable, 'The portable flag should be set to true')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, [], 'The service file should be blank')
def parse_options_all_no_file_test(self):
"""
Test the parse options process works with two options
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug', '-d']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertTrue(args.dev_version, 'The dev_version flag should be True')
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, [], 'The service file should be blank')
def parse_options_file_test(self):
"""
Test the parse options process works with a file
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['dummy_temp']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
def parse_options_file_and_debug_test(self):
"""
Test the parse options process works with a file
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['-l debug', 'dummy_temp']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
self.assertFalse(args.portable, 'The portable flag should be set to false')
self.assertEquals(args.style, None, 'There are no style flags to be processed')
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
def parse_options_two_files_test(self):
"""
Test the parse options process works with a file
"""
# GIVEN: a a set of system arguments.
sys.argv[1:] = ['dummy_temp', 'dummy_temp2']
# WHEN: We we parse them to expand to options
args = parse_options()
# THEN: the following fields will have been extracted.
self.assertEquals(args, None, 'The args should be None')

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