This commit is contained in:
Tim Bentley 2016-01-11 21:12:03 +00:00
commit 2218ecbcd7
66 changed files with 360 additions and 285 deletions

View File

@ -43,5 +43,4 @@ __pycache__
.coverage .coverage
cover cover
*.kdev4 *.kdev4
./.coveragerc coverage
./coverage

5
.coveragerc Normal file
View File

@ -0,0 +1,5 @@
[run]
source = openlp
[html]
directory = coverage

View File

@ -1 +1 @@
2.3.1 2.3.2

View File

@ -263,7 +263,8 @@ class Plugin(QtCore.QObject, RegistryProperties):
else: else:
self.media_item.on_add_click() self.media_item.on_add_click()
def about(self): @staticmethod
def about():
""" """
Show a dialog when the user clicks on the 'About' button in the plugin manager. Show a dialog when the user clicks on the 'About' button in the plugin manager.
""" """

View File

@ -273,7 +273,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
except ValueError: except ValueError:
text_to_render = text.split('\n[---]\n')[0] text_to_render = text.split('\n[---]\n')[0]
text = '' text = ''
text_to_render, raw_tags, html_tags = self._get_start_tags(text_to_render) text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
if text: if text:
text = raw_tags + text text = raw_tags + text
else: else:
@ -441,7 +441,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
previous_raw = line + line_end previous_raw = line + line_end
continue continue
# Figure out how many words of the line will fit on screen as the line will not fit as a whole. # Figure out how many words of the line will fit on screen as the line will not fit as a whole.
raw_words = self._words_split(line) raw_words = Renderer.words_split(line)
html_words = list(map(expand_tags, raw_words)) html_words = list(map(expand_tags, raw_words))
previous_html, previous_raw = \ previous_html, previous_raw = \
self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end) self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
@ -451,42 +451,6 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
formatted.append(previous_raw) formatted.append(previous_raw)
return formatted return formatted
def _get_start_tags(self, raw_text):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
:param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
are allowed::
{st}{r}Text text text
"""
raw_tags = []
html_tags = []
for tag in FormattingTags.get_html_tags():
if tag['start tag'] == '{br}':
continue
if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# opened first on the next slide as well.
raw_tags.sort(key=lambda tag: tag[0])
html_tags.sort(key=lambda tag: tag[0])
# Create a list with closing tags for the raw_text.
end_tags = []
start_tags = []
for tag in raw_tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
# Remove the indexes.
html_tags = [tag[1] for tag in html_tags]
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end): def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
""" """
This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line) This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
@ -521,7 +485,7 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
if smallest_index == index or highest_index == index: if smallest_index == index or highest_index == index:
index = smallest_index index = smallest_index
text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1]) text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
text, raw_tags, html_tags = self._get_start_tags(text) text, raw_tags, html_tags = get_start_tags(text)
formatted.append(text) formatted.append(text)
previous_html = '' previous_html = ''
previous_raw = '' previous_raw = ''
@ -556,7 +520,8 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
self.web_frame.evaluateJavaScript('show_text("%s")' % text.replace('\\', '\\\\').replace('\"', '\\\"')) self.web_frame.evaluateJavaScript('show_text("%s")' % text.replace('\\', '\\\\').replace('\"', '\\\"'))
return self.web_frame.contentsSize().height() <= self.empty_height return self.web_frame.contentsSize().height() <= self.empty_height
def _words_split(self, line):
def words_split(line):
""" """
Split the slide up by word so can wrap better Split the slide up by word so can wrap better
@ -565,3 +530,40 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
# this parse we are to be wordy # this parse we are to be wordy
line = line.replace('\n', ' ') line = line.replace('\n', ' ')
return line.split(' ') return line.split(' ')
def get_start_tags(raw_text):
"""
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
formatting tags and the third unicode string the html opening formatting tags.
:param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
are allowed::
{st}{r}Text text text
"""
raw_tags = []
html_tags = []
for tag in FormattingTags.get_html_tags():
if tag['start tag'] == '{br}':
continue
if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
# opened first on the next slide as well.
raw_tags.sort(key=lambda tag: tag[0])
html_tags.sort(key=lambda tag: tag[0])
# Create a list with closing tags for the raw_text.
end_tags = []
start_tags = []
for tag in raw_tags:
start_tags.append(tag[1])
end_tags.append(tag[2])
end_tags.reverse()
# Remove the indexes.
html_tags = [tag[1] for tag in html_tags]
return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)

View File

@ -40,8 +40,7 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
""" """
Do some initialisation stuff Do some initialisation stuff
""" """
super(AboutForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(AboutForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self._setup() self._setup()
def _setup(self): def _setup(self):

View File

@ -89,8 +89,7 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
""" """
Constructor. Constructor.
""" """
super(ExceptionForm, self).__init__(None, QtCore.Qt.WindowSystemMenuHint super(ExceptionForm, self).__init__(None, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.settings_section = 'crashreport' self.settings_section = 'crashreport'
self.report_text = '**OpenLP Bug Report**\n' \ self.report_text = '**OpenLP Bug Report**\n' \

View File

@ -37,8 +37,7 @@ class FirstTimeLanguageForm(QtWidgets.QDialog, Ui_FirstTimeLanguageDialog):
""" """
Constructor Constructor
""" """
super(FirstTimeLanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(FirstTimeLanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.qm_list = LanguageManager.get_qm_list() self.qm_list = LanguageManager.get_qm_list()
self.language_combo_box.addItem('Autodetect') self.language_combo_box.addItem('Autodetect')

View File

@ -51,8 +51,7 @@ class FormattingTagForm(QtWidgets.QDialog, Ui_FormattingTagDialog, FormattingTag
""" """
Constructor Constructor
""" """
super(FormattingTagForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(FormattingTagForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self._setup() self._setup()

View File

@ -47,6 +47,7 @@ from openlp.core.utils import LanguageManager, add_actions, get_application_vers
from openlp.core.utils.actions import ActionList, CategoryOrder from openlp.core.utils.actions import ActionList, CategoryOrder
from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.projector.manager import ProjectorManager from openlp.core.ui.projector.manager import ProjectorManager
from openlp.core.ui.printserviceform import PrintServiceForm
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -198,7 +199,7 @@ class Ui_MainWindow(object):
triggers=self.service_manager_contents.save_file_as) triggers=self.service_manager_contents.save_file_as)
self.print_service_order_item = create_action(main_window, 'printServiceItem', can_shortcuts=True, self.print_service_order_item = create_action(main_window, 'printServiceItem', can_shortcuts=True,
category=UiStrings().File, category=UiStrings().File,
triggers=self.service_manager_contents.print_service_order) triggers=lambda x: PrintServiceForm().exec())
self.file_exit_item = create_action(main_window, 'fileExitItem', icon=':/system/system_exit.png', self.file_exit_item = create_action(main_window, 'fileExitItem', icon=':/system/system_exit.png',
can_shortcuts=True, can_shortcuts=True,
category=UiStrings().File, triggers=main_window.close) category=UiStrings().File, triggers=main_window.close)

View File

@ -326,10 +326,10 @@ class WebkitPlayer(MediaPlayer):
controller = display.controller controller = display.controller
if controller.media_info.is_flash: if controller.media_info.is_flash:
seek = seek_value seek = seek_value
display.frame.evaluateJavaScript('show_flash("seek", null, null, "%s");' % (seek)) display.frame.evaluateJavaScript('show_flash("seek", null, null, "%s");' % seek)
else: else:
seek = float(seek_value) / 1000 seek = float(seek_value) / 1000
display.frame.evaluateJavaScript('show_video("seek", null, null, null, "%f");' % (seek)) display.frame.evaluateJavaScript('show_video("seek", null, null, null, "%f");' % seek)
def reset(self, display): def reset(self, display):
""" """

View File

@ -41,8 +41,7 @@ class PluginForm(QtWidgets.QDialog, Ui_PluginViewDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(PluginForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(PluginForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.active_plugin = None self.active_plugin = None
self.programatic_change = False self.programatic_change = False
self.setupUi(self) self.setupUi(self)

View File

@ -144,8 +144,7 @@ class ProjectorEditForm(QDialog, Ui_ProjectorEditForm):
editProjector = pyqtSignal(object) editProjector = pyqtSignal(object)
def __init__(self, parent=None, projectordb=None): def __init__(self, parent=None, projectordb=None):
super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(ProjectorEditForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.projectordb = projectordb self.projectordb = projectordb
self.setupUi(self) self.setupUi(self)
self.button_box.accepted.connect(self.accept_me) self.button_box.accepted.connect(self.accept_me)

View File

@ -236,8 +236,7 @@ class SourceSelectTabs(QDialog):
:param projectordb: ProjectorDB session to use :param projectordb: ProjectorDB session to use
""" """
log.debug('Initializing SourceSelectTabs()') log.debug('Initializing SourceSelectTabs()')
super(SourceSelectTabs, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SourceSelectTabs, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setMinimumWidth(350) self.setMinimumWidth(350)
self.projectordb = projectordb self.projectordb = projectordb
self.edit = edit self.edit = edit
@ -386,8 +385,7 @@ class SourceSelectSingle(QDialog):
""" """
log.debug('Initializing SourceSelectSingle()') log.debug('Initializing SourceSelectSingle()')
self.projectordb = projectordb self.projectordb = projectordb
super(SourceSelectSingle, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SourceSelectSingle, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.edit = edit self.edit = edit
if self.edit: if self.edit:
title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text') title = translate('OpenLP.SourceSelectForm', 'Edit Projector Source Text')

View File

@ -144,8 +144,8 @@ class Ui_ServiceManager(object):
self.service_manager_list.customContextMenuRequested.connect(self.context_menu) self.service_manager_list.customContextMenuRequested.connect(self.context_menu)
self.service_manager_list.setObjectName('service_manager_list') self.service_manager_list.setObjectName('service_manager_list')
# enable drop # enable drop
self.service_manager_list.__class__.dragEnterEvent = self.drag_enter_event self.service_manager_list.__class__.dragEnterEvent = lambda x, event: event.accept()
self.service_manager_list.__class__.dragMoveEvent = self.drag_enter_event self.service_manager_list.__class__.dragMoveEvent = lambda x, event: event.accept()
self.service_manager_list.__class__.dropEvent = self.drop_event self.service_manager_list.__class__.dropEvent = self.drop_event
self.layout.addWidget(self.service_manager_list) self.layout.addWidget(self.service_manager_list)
# Add the bottom toolbar # Add the bottom toolbar
@ -293,14 +293,6 @@ class Ui_ServiceManager(object):
Registry().register_function('theme_update_global', self.theme_change) Registry().register_function('theme_update_global', self.theme_change)
Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes) Registry().register_function('mediaitem_suffix_reset', self.reset_supported_suffixes)
def drag_enter_event(self, event):
"""
Accept Drag events
:param event: Handle of the event passed
"""
event.accept()
class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceManager, RegistryProperties): class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceManager, RegistryProperties):
""" """
@ -1585,7 +1577,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
if item is None: if item is None:
end_pos = len(self.service_items) end_pos = len(self.service_items)
else: else:
end_pos = self._get_parent_item_data(item) - 1 end_pos = get_parent_item_data(item) - 1
service_item = self.service_items[start_pos] service_item = self.service_items[start_pos]
self.service_items.remove(service_item) self.service_items.remove(service_item)
self.service_items.insert(end_pos, service_item) self.service_items.insert(end_pos, service_item)
@ -1598,21 +1590,21 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.drop_position = len(self.service_items) self.drop_position = len(self.service_items)
else: else:
# we are over something so lets investigate # we are over something so lets investigate
pos = self._get_parent_item_data(item) - 1 pos = get_parent_item_data(item) - 1
service_item = self.service_items[pos] service_item = self.service_items[pos]
if (plugin == service_item['service_item'].name and if (plugin == service_item['service_item'].name and
service_item['service_item'].is_capable(ItemCapabilities.CanAppend)): service_item['service_item'].is_capable(ItemCapabilities.CanAppend)):
action = self.dnd_menu.exec(QtGui.QCursor.pos()) action = self.dnd_menu.exec(QtGui.QCursor.pos())
# New action required # New action required
if action == self.new_action: if action == self.new_action:
self.drop_position = self._get_parent_item_data(item) self.drop_position = get_parent_item_data(item)
# Append to existing action # Append to existing action
if action == self.add_to_action: if action == self.add_to_action:
self.drop_position = self._get_parent_item_data(item) self.drop_position = get_parent_item_data(item)
item.setSelected(True) item.setSelected(True)
replace = True replace = True
else: else:
self.drop_position = self._get_parent_item_data(item) - 1 self.drop_position = get_parent_item_data(item) - 1
Registry().execute('%s_add_service_item' % plugin, replace) Registry().execute('%s_add_service_item' % plugin, replace)
def update_theme_list(self, theme_list): def update_theme_list(self, theme_list):
@ -1656,7 +1648,14 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self.service_items[item]['service_item'].update_theme(theme) self.service_items[item]['service_item'].update_theme(theme)
self.regenerate_service_items(True) self.regenerate_service_items(True)
def _get_parent_item_data(self, item): def get_drop_position(self):
"""
Getter for drop_position. Used in: MediaManagerItem
"""
return self.drop_position
def get_parent_item_data(item):
""" """
Finds and returns the parent item for any item Finds and returns the parent item for any item
@ -1667,16 +1666,3 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
return item.data(0, QtCore.Qt.UserRole) return item.data(0, QtCore.Qt.UserRole)
else: else:
return parent_item.data(0, QtCore.Qt.UserRole) return parent_item.data(0, QtCore.Qt.UserRole)
def print_service_order(self, field=None):
"""
Print a Service Order Sheet.
"""
setting_dialog = PrintServiceForm()
setting_dialog.exec()
def get_drop_position(self):
"""
Getter for drop_position. Used in: MediaManagerItem
"""
return self.drop_position

View File

@ -46,8 +46,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
""" """
Registry().register('settings_form', self) Registry().register('settings_form', self)
Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up) Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
super(SettingsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SettingsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.processes = [] self.processes = []
self.setupUi(self) self.setupUi(self)
self.setting_list_widget.currentRowChanged.connect(self.list_item_changed) self.setting_list_widget.currentRowChanged.connect(self.list_item_changed)

View File

@ -44,8 +44,7 @@ class ShortcutListForm(QtWidgets.QDialog, Ui_ShortcutListDialog, RegistryPropert
""" """
Constructor Constructor
""" """
super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(ShortcutListForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.changed_actions = {} self.changed_actions = {}
self.action_list = ActionList.get_instance() self.action_list = ActionList.get_instance()

View File

@ -191,7 +191,8 @@ class AlertsPlugin(Plugin):
self.alert_form.load_list() self.alert_form.load_list()
self.alert_form.exec() self.alert_form.exec()
def about(self): @staticmethod
def about():
""" """
Plugin Alerts about method Plugin Alerts about method
@ -215,7 +216,8 @@ class AlertsPlugin(Plugin):
'title': translate('AlertsPlugin', 'Alerts', 'container title') 'title': translate('AlertsPlugin', 'Alerts', 'container title')
} }
def get_display_javascript(self): @staticmethod
def get_display_javascript():
""" """
Add Javascript to the main display. Add Javascript to the main display.
""" """
@ -229,7 +231,8 @@ class AlertsPlugin(Plugin):
return CSS % (align, self.settings_tab.font_face, self.settings_tab.font_size, self.settings_tab.font_color, return CSS % (align, self.settings_tab.font_face, self.settings_tab.font_size, self.settings_tab.font_color,
self.settings_tab.background_color) self.settings_tab.background_color)
def get_display_html(self): @staticmethod
def get_display_html():
""" """
Add HTML to the main display. Add HTML to the main display.
""" """

View File

@ -167,7 +167,8 @@ class BiblePlugin(Plugin):
if self.media_item: if self.media_item:
self.media_item.on_import_click() self.media_item.on_import_click()
def about(self): @staticmethod
def about():
""" """
Return the about text for the plugin manager Return the about text for the plugin manager
""" """

View File

@ -69,8 +69,8 @@ class BibleImportForm(OpenLPWizard):
""" """
self.manager = manager self.manager = manager
self.web_bible_list = {} self.web_bible_list = {}
super(BibleImportForm, self).__init__( super(BibleImportForm, self).__init__(parent, bible_plugin,
parent, bible_plugin, 'bibleImportWizard', ':/wizards/wizard_importbible.bmp') 'bibleImportWizard', ':/wizards/wizard_importbible.bmp')
def setupUi(self, image): def setupUi(self, image):
""" """

View File

@ -49,8 +49,7 @@ class BookNameForm(QDialog, Ui_BookNameDialog):
""" """
Constructor Constructor
""" """
super(BookNameForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(BookNameForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.custom_signals() self.custom_signals()
self.book_names = BibleStrings().BookNames self.book_names = BibleStrings().BookNames

View File

@ -45,8 +45,7 @@ class EditBibleForm(QtWidgets.QDialog, Ui_EditBibleDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(EditBibleForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(EditBibleForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.media_item = media_item self.media_item = media_item
self.book_names = BibleStrings().BookNames self.book_names = BibleStrings().BookNames
self.setupUi(self) self.setupUi(self)

View File

@ -47,8 +47,7 @@ class LanguageForm(QDialog, Ui_LanguageDialog):
""" """
Constructor Constructor
""" """
super(LanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(LanguageForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def exec(self, bible_name): def exec(self, bible_name):

View File

@ -62,7 +62,8 @@ class CustomPlugin(Plugin):
self.icon_path = ':/plugins/plugin_custom.png' self.icon_path = ':/plugins/plugin_custom.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
def about(self): @staticmethod
def about():
about_text = translate('CustomPlugin', '<strong>Custom Slide Plugin </strong><br />The custom slide plugin ' about_text = translate('CustomPlugin', '<strong>Custom Slide Plugin </strong><br />The custom slide plugin '
'provides the ability to set up custom text slides that can be displayed on the screen ' 'provides the ability to set up custom text slides that can be displayed on the screen '
'the same way songs are. This plugin provides greater freedom over the songs plugin.') 'the same way songs are. This plugin provides greater freedom over the songs plugin.')
@ -73,6 +74,7 @@ class CustomPlugin(Plugin):
Called to find out if the custom plugin is currently using a theme. Called to find out if the custom plugin is currently using a theme.
Returns count of the times the theme is used. Returns count of the times the theme is used.
:param theme: Theme to be queried
""" """
return len(self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme)) return len(self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme))

View File

@ -44,8 +44,7 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog):
""" """
Constructor Constructor
""" """
super(EditCustomForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(EditCustomForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.manager = manager self.manager = manager
self.media_item = media_item self.media_item = media_item
self.setupUi(self) self.setupUi(self)

View File

@ -39,8 +39,7 @@ class EditCustomSlideForm(QtWidgets.QDialog, Ui_CustomSlideEditDialog):
""" """
Constructor Constructor
""" """
super(EditCustomSlideForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(EditCustomSlideForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
# Connecting signals and slots # Connecting signals and slots
self.insert_button.clicked.connect(self.on_insert_button_clicked) self.insert_button.clicked.connect(self.on_insert_button_clicked)

View File

@ -35,8 +35,7 @@ class AddGroupForm(QtWidgets.QDialog, Ui_AddGroupDialog):
""" """
Constructor Constructor
""" """
super(AddGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(AddGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def exec(self, clear=True, show_top_level_group=False, selected_group=None): def exec(self, clear=True, show_top_level_group=False, selected_group=None):

View File

@ -33,8 +33,7 @@ class ChooseGroupForm(QtWidgets.QDialog, Ui_ChooseGroupDialog):
""" """
Constructor Constructor
""" """
super(ChooseGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(ChooseGroupForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def exec(self, selected_group=None): def exec(self, selected_group=None):

View File

@ -23,9 +23,8 @@
from PyQt5 import QtGui from PyQt5 import QtGui
import logging import logging
import os
from openlp.core.common import Registry, Settings, translate from openlp.core.common import Settings, translate
from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.plugins.images.lib import ImageMediaItem, ImageTab from openlp.plugins.images.lib import ImageMediaItem, ImageTab
@ -53,7 +52,8 @@ class ImagePlugin(Plugin):
self.icon_path = ':/plugins/plugin_images.png' self.icon_path = ':/plugins/plugin_images.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
def about(self): @staticmethod
def about():
about_text = translate('ImagePlugin', '<strong>Image Plugin</strong>' about_text = translate('ImagePlugin', '<strong>Image Plugin</strong>'
'<br />The image plugin provides displaying of images.<br />One ' '<br />The image plugin provides displaying of images.<br />One '
'of the distinguishing features of this plugin is the ability to ' 'of the distinguishing features of this plugin is the ability to '

View File

@ -52,8 +52,7 @@ class MediaClipSelectorForm(QtWidgets.QDialog, Ui_MediaClipSelector, RegistryPro
""" """
Constructor Constructor
""" """
super(MediaClipSelectorForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(MediaClipSelectorForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.vlc_instance = None self.vlc_instance = None
self.vlc_media_player = None self.vlc_media_player = None
self.vlc_media = None self.vlc_media = None

View File

@ -84,7 +84,8 @@ class MediaPlugin(Plugin):
visible_name = self.get_string(StringContent.VisibleName) visible_name = self.get_string(StringContent.VisibleName)
self.settings_tab = MediaTab(parent, self.name, visible_name['title'], self.icon_path) self.settings_tab = MediaTab(parent, self.name, visible_name['title'], self.icon_path)
def about(self): @staticmethod
def about():
""" """
Return the about text for the plugin manager Return the about text for the plugin manager
""" """

View File

@ -28,7 +28,7 @@ import logging
from PyQt5 import QtCore from PyQt5 import QtCore
from openlp.core.common import AppLocation, Settings, translate from openlp.core.common import AppLocation, translate
from openlp.core.lib import Plugin, StringContent, build_icon from openlp.core.lib import Plugin, StringContent, build_icon
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
@ -71,6 +71,7 @@ class PresentationPlugin(Plugin):
def create_settings_tab(self, parent): def create_settings_tab(self, parent):
""" """
Create the settings Tab. Create the settings Tab.
:param parent: parent UI Element
""" """
visible_name = self.get_string(StringContent.VisibleName) visible_name = self.get_string(StringContent.VisibleName)
self.settings_tab = PresentationTab(parent, self.name, visible_name['title'], self.controllers, self.icon_path) self.settings_tab = PresentationTab(parent, self.name, visible_name['title'], self.controllers, self.icon_path)
@ -112,6 +113,7 @@ class PresentationPlugin(Plugin):
def register_controllers(self, controller): def register_controllers(self, controller):
""" """
Register each presentation controller (Impress, PPT etc) and store for later use. Register each presentation controller (Impress, PPT etc) and store for later use.
:param controller: controller to register
""" """
self.controllers[controller.name] = controller self.controllers[controller.name] = controller
@ -137,7 +139,8 @@ class PresentationPlugin(Plugin):
self.register_controllers(controller) self.register_controllers(controller)
return bool(self.controllers) return bool(self.controllers)
def about(self): @staticmethod
def about():
""" """
Return information about this plugin. Return information about this plugin.
""" """

View File

@ -88,7 +88,8 @@ class RemotesPlugin(Plugin):
self.server.stop_server() self.server.stop_server()
self.server = None self.server = None
def about(self): @staticmethod
def about():
""" """
Information about this plugin Information about this plugin
""" """

View File

@ -35,8 +35,7 @@ class AuthorsForm(QtWidgets.QDialog, Ui_AuthorsDialog):
""" """
Set up the screen and common data Set up the screen and common data
""" """
super(AuthorsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(AuthorsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.auto_display_name = False self.auto_display_name = False
self.first_name_edit.textEdited.connect(self.on_first_name_edited) self.first_name_edit.textEdited.connect(self.on_first_name_edited)

View File

@ -55,8 +55,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties):
""" """
Constructor Constructor
""" """
super(EditSongForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(EditSongForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.media_item = media_item self.media_item = media_item
self.song = None self.song = None
# can this be automated? # can this be automated?

View File

@ -41,8 +41,7 @@ class EditVerseForm(QtWidgets.QDialog, Ui_EditVerseDialog):
""" """
Constructor Constructor
""" """
super(EditVerseForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(EditVerseForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.has_single_verse = False self.has_single_verse = False
self.insert_button.clicked.connect(self.on_insert_button_clicked) self.insert_button.clicked.connect(self.on_insert_button_clicked)

View File

@ -37,8 +37,7 @@ class MediaFilesForm(QtWidgets.QDialog, Ui_MediaFilesDialog):
log.info('%s MediaFilesForm loaded', __name__) log.info('%s MediaFilesForm loaded', __name__)
def __init__(self, parent): def __init__(self, parent):
super(MediaFilesForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(MediaFilesForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def populate_files(self, files): def populate_files(self, files):

View File

@ -38,8 +38,7 @@ class SongBookForm(QtWidgets.QDialog, Ui_SongBookDialog):
""" """
Constructor Constructor
""" """
super(SongBookForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SongBookForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def exec(self, clear=True): def exec(self, clear=True):

View File

@ -72,7 +72,7 @@ class SongExportForm(OpenLPWizard):
""" """
Song wizard specific signals. Song wizard specific signals.
""" """
self.available_list_widget.itemActivated.connect(self.on_item_activated) self.available_list_widget.itemActivated.connect(on_item_activated)
self.search_line_edit.textEdited.connect(self.on_search_line_edit_changed) self.search_line_edit.textEdited.connect(self.on_search_line_edit_changed)
self.uncheck_button.clicked.connect(self.on_uncheck_button_clicked) self.uncheck_button.clicked.connect(self.on_uncheck_button_clicked)
self.check_button.clicked.connect(self.on_check_button_clicked) self.check_button.clicked.connect(self.on_check_button_clicked)
@ -172,7 +172,7 @@ class SongExportForm(OpenLPWizard):
return True return True
elif self.currentPage() == self.available_songs_page: elif self.currentPage() == self.available_songs_page:
items = [ items = [
item for item in self._find_list_widget_items(self.available_list_widget) if item.checkState() item for item in find_list_widget_items(self.available_list_widget) if item.checkState()
] ]
if not items: if not items:
critical_error_message_box( critical_error_message_box(
@ -241,7 +241,7 @@ class SongExportForm(OpenLPWizard):
""" """
songs = [ songs = [
song.data(QtCore.Qt.UserRole) song.data(QtCore.Qt.UserRole)
for song in self._find_list_widget_items(self.selected_list_widget) for song in find_list_widget_items(self.selected_list_widget)
] ]
exporter = OpenLyricsExport(self, songs, self.directory_line_edit.text()) exporter = OpenLyricsExport(self, songs, self.directory_line_edit.text())
try: try:
@ -255,28 +255,6 @@ class SongExportForm(OpenLPWizard):
self.progress_label.setText(translate('SongsPlugin.SongExportForm', 'Your song export failed because this ' self.progress_label.setText(translate('SongsPlugin.SongExportForm', 'Your song export failed because this '
'error occurred: %s') % ose.strerror) 'error occurred: %s') % ose.strerror)
def _find_list_widget_items(self, list_widget, text=''):
"""
Returns a list of *QListWidgetItem*s of the ``list_widget``. Note, that hidden items are included.
:param list_widget: The widget to get all items from. (QListWidget)
:param text: The text to search for. (unicode string)
"""
return [
item for item in list_widget.findItems(text, QtCore.Qt.MatchContains)
]
def on_item_activated(self, item):
"""
Called, when an item in the *available_list_widget* has been triggered.
The item is check if it was not checked, whereas it is unchecked when it
was checked.
:param item: The *QListWidgetItem* which was triggered.
"""
item.setCheckState(
QtCore.Qt.Unchecked if item.checkState() else QtCore.Qt.Checked)
def on_search_line_edit_changed(self, text): def on_search_line_edit_changed(self, text):
""" """
The *search_line_edit*'s text has been changed. Update the list of The *search_line_edit*'s text has been changed. Update the list of
@ -286,9 +264,9 @@ class SongExportForm(OpenLPWizard):
:param text: The text of the *search_line_edit*. :param text: The text of the *search_line_edit*.
""" """
search_result = [ search_result = [
song for song in self._find_list_widget_items(self.available_list_widget, text) song for song in find_list_widget_items(self.available_list_widget, text)
] ]
for item in self._find_list_widget_items(self.available_list_widget): for item in find_list_widget_items(self.available_list_widget):
item.setHidden(item not in search_result) item.setHidden(item not in search_result)
def on_uncheck_button_clicked(self): def on_uncheck_button_clicked(self):
@ -317,3 +295,26 @@ class SongExportForm(OpenLPWizard):
self.get_folder( self.get_folder(
translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'), translate('SongsPlugin.ExportWizardForm', 'Select Destination Folder'),
self.directory_line_edit, 'last directory export') self.directory_line_edit, 'last directory export')
def find_list_widget_items(list_widget, text=''):
"""
Returns a list of *QListWidgetItem*s of the ``list_widget``. Note, that hidden items are included.
:param list_widget: The widget to get all items from. (QListWidget)
:param text: The text to search for. (unicode string)
"""
return [
item for item in list_widget.findItems(text, QtCore.Qt.MatchContains)
]
def on_item_activated(item):
"""
Called, when an item in the *available_list_widget* has been triggered.
The item is check if it was not checked, whereas it is unchecked when it
was checked.
:param item: The *QListWidgetItem* which was triggered.
"""
item.setCheckState(QtCore.Qt.Unchecked if item.checkState() else QtCore.Qt.Checked)

View File

@ -44,8 +44,7 @@ class SongMaintenanceForm(QtWidgets.QDialog, Ui_SongMaintenanceDialog, RegistryP
""" """
Constructor Constructor
""" """
super(SongMaintenanceForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SongMaintenanceForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.manager = manager self.manager = manager
self.author_form = AuthorsForm(self) self.author_form = AuthorsForm(self)

View File

@ -81,8 +81,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog):
""" """
def __init__(self, parent=None, plugin=None, db_manager=None): def __init__(self, parent=None, plugin=None, db_manager=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.plugin = plugin self.plugin = plugin
self.db_manager = db_manager self.db_manager = db_manager
self.setup_ui(self) self.setup_ui(self)

View File

@ -38,8 +38,7 @@ class TopicsForm(QtWidgets.QDialog, Ui_TopicsDialog):
""" """
Constructor Constructor
""" """
super(TopicsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(TopicsForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
def exec(self, clear=True): def exec(self, clear=True):

View File

@ -390,7 +390,7 @@ class SongFormat(object):
""" """
Return a list of the supported song formats. Return a list of the supported song formats.
""" """
return [ return sorted([
SongFormat.OpenLyrics, SongFormat.OpenLyrics,
SongFormat.OpenLP2, SongFormat.OpenLP2,
SongFormat.Generic, SongFormat.Generic,
@ -400,6 +400,7 @@ class SongFormat(object):
SongFormat.EasyWorshipDB, SongFormat.EasyWorshipDB,
SongFormat.EasyWorshipService, SongFormat.EasyWorshipService,
SongFormat.FoilPresenter, SongFormat.FoilPresenter,
SongFormat.Lyrix,
SongFormat.MediaShout, SongFormat.MediaShout,
SongFormat.OpenSong, SongFormat.OpenSong,
SongFormat.PowerPraise, SongFormat.PowerPraise,
@ -411,13 +412,12 @@ class SongFormat(object):
SongFormat.SongShowPlus, SongFormat.SongShowPlus,
SongFormat.SongsOfFellowship, SongFormat.SongsOfFellowship,
SongFormat.SundayPlus, SongFormat.SundayPlus,
SongFormat.VideoPsalm,
SongFormat.WordsOfWorship, SongFormat.WordsOfWorship,
SongFormat.WorshipAssistant, SongFormat.WorshipAssistant,
SongFormat.WorshipCenterPro, SongFormat.WorshipCenterPro,
SongFormat.ZionWorx, SongFormat.ZionWorx,
SongFormat.Lyrix, ])
SongFormat.VideoPsalm
]
@staticmethod @staticmethod
def get(song_format, *attributes): def get(song_format, *attributes):

View File

@ -234,16 +234,6 @@ class FoilPresenter(object):
clean_song(self.manager, song) clean_song(self.manager, song)
self.manager.save_object(song) self.manager.save_object(song)
def _child(self, element):
"""
This returns the text of an element as unicode string.
:param element: The element
"""
if element is not None:
return str(element)
return ''
def _process_authors(self, foilpresenterfolie, song): def _process_authors(self, foilpresenterfolie, song):
""" """
Adds the authors specified in the XML to the song. Adds the authors specified in the XML to the song.
@ -253,7 +243,7 @@ class FoilPresenter(object):
""" """
authors = [] authors = []
try: try:
copyright = self._child(foilpresenterfolie.copyright.text_) copyright = to_str(foilpresenterfolie.copyright.text_)
except AttributeError: except AttributeError:
copyright = None copyright = None
if copyright: if copyright:
@ -346,7 +336,7 @@ class FoilPresenter(object):
:param song: The song object. :param song: The song object.
""" """
try: try:
song.ccli_number = self._child(foilpresenterfolie.ccliid) song.ccli_number = to_str(foilpresenterfolie.ccliid)
except AttributeError: except AttributeError:
song.ccli_number = '' song.ccli_number = ''
@ -358,7 +348,7 @@ class FoilPresenter(object):
:param song: The song object. :param song: The song object.
""" """
try: try:
song.comments = self._child(foilpresenterfolie.notiz) song.comments = to_str(foilpresenterfolie.notiz)
except AttributeError: except AttributeError:
song.comments = '' song.comments = ''
@ -370,7 +360,7 @@ class FoilPresenter(object):
:param song: The song object. :param song: The song object.
""" """
try: try:
song.copyright = self._child(foilpresenterfolie.copyright.text_) song.copyright = to_str(foilpresenterfolie.copyright.text_)
except AttributeError: except AttributeError:
song.copyright = '' song.copyright = ''
@ -396,19 +386,19 @@ class FoilPresenter(object):
VerseType.tags[VerseType.PreChorus]: 1 VerseType.tags[VerseType.PreChorus]: 1
} }
if not hasattr(foilpresenterfolie.strophen, 'strophe'): if not hasattr(foilpresenterfolie.strophen, 'strophe'):
self.importer.log_error(self._child(foilpresenterfolie.titel), self.importer.log_error(to_str(foilpresenterfolie.titel),
str(translate('SongsPlugin.FoilPresenterSongImport', str(translate('SongsPlugin.FoilPresenterSongImport',
'Invalid Foilpresenter song file. No verses found.'))) 'Invalid Foilpresenter song file. No verses found.')))
self.save_song = False self.save_song = False
return return
for strophe in foilpresenterfolie.strophen.strophe: for strophe in foilpresenterfolie.strophen.strophe:
text = self._child(strophe.text_) if hasattr(strophe, 'text_') else '' text = to_str(strophe.text_) if hasattr(strophe, 'text_') else ''
verse_name = self._child(strophe.key) verse_name = to_str(strophe.key)
children = strophe.getchildren() children = strophe.getchildren()
sortnr = False sortnr = False
for child in children: for child in children:
if child.tag == 'sortnr': if child.tag == 'sortnr':
verse_sortnr = self._child(strophe.sortnr) verse_sortnr = to_str(strophe.sortnr)
sortnr = True sortnr = True
# In older Version there is no sortnr, but we need one # In older Version there is no sortnr, but we need one
if not sortnr: if not sortnr:
@ -484,7 +474,7 @@ class FoilPresenter(object):
song.song_number = '' song.song_number = ''
try: try:
for bucheintrag in foilpresenterfolie.buch.bucheintrag: for bucheintrag in foilpresenterfolie.buch.bucheintrag:
book_name = self._child(bucheintrag.name) book_name = to_str(bucheintrag.name)
if book_name: if book_name:
book = self.manager.get_object_filtered(Book, Book.name == book_name) book = self.manager.get_object_filtered(Book, Book.name == book_name)
if book is None: if book is None:
@ -493,8 +483,8 @@ class FoilPresenter(object):
self.manager.save_object(book) self.manager.save_object(book)
song.song_book_id = book.id song.song_book_id = book.id
try: try:
if self._child(bucheintrag.nummer): if to_str(bucheintrag.nummer):
song.song_number = self._child(bucheintrag.nummer) song.song_number = to_str(bucheintrag.nummer)
except AttributeError: except AttributeError:
pass pass
# We only support one song book, so take the first one. # We only support one song book, so take the first one.
@ -512,13 +502,13 @@ class FoilPresenter(object):
try: try:
for title_string in foilpresenterfolie.titel.titelstring: for title_string in foilpresenterfolie.titel.titelstring:
if not song.title: if not song.title:
song.title = self._child(title_string) song.title = to_str(title_string)
song.alternate_title = '' song.alternate_title = ''
else: else:
song.alternate_title = self._child(title_string) song.alternate_title = to_str(title_string)
except AttributeError: except AttributeError:
# Use first line of first verse # Use first line of first verse
first_line = self._child(foilpresenterfolie.strophen.strophe.text_) first_line = to_str(foilpresenterfolie.strophen.strophe.text_)
song.title = first_line.split('\n')[0] song.title = first_line.split('\n')[0]
def _process_topics(self, foilpresenterfolie, song): def _process_topics(self, foilpresenterfolie, song):
@ -530,7 +520,7 @@ class FoilPresenter(object):
""" """
try: try:
for name in foilpresenterfolie.kategorien.name: for name in foilpresenterfolie.kategorien.name:
topic_text = self._child(name) topic_text = to_str(name)
if topic_text: if topic_text:
topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text) topic = self.manager.get_object_filtered(Topic, Topic.name == topic_text)
if topic is None: if topic is None:
@ -540,3 +530,14 @@ class FoilPresenter(object):
song.topics.append(topic) song.topics.append(topic)
except AttributeError: except AttributeError:
pass pass
def to_str(element):
"""
This returns the text of an element as unicode string.
:param element: The element
"""
if element is not None:
return str(element)
return ''

View File

@ -36,28 +36,28 @@ log = logging.getLogger(__name__)
class SongBeamerTypes(object): class SongBeamerTypes(object):
MarkTypes = { MarkTypes = {
'Refrain': VerseType.tags[VerseType.Chorus], 'refrain': VerseType.tags[VerseType.Chorus],
'Chorus': VerseType.tags[VerseType.Chorus], 'chorus': VerseType.tags[VerseType.Chorus],
'Vers': VerseType.tags[VerseType.Verse], 'vers': VerseType.tags[VerseType.Verse],
'Verse': VerseType.tags[VerseType.Verse], 'verse': VerseType.tags[VerseType.Verse],
'Strophe': VerseType.tags[VerseType.Verse], 'strophe': VerseType.tags[VerseType.Verse],
'Intro': VerseType.tags[VerseType.Intro], 'intro': VerseType.tags[VerseType.Intro],
'Coda': VerseType.tags[VerseType.Ending], 'coda': VerseType.tags[VerseType.Ending],
'Ending': VerseType.tags[VerseType.Ending], 'ending': VerseType.tags[VerseType.Ending],
'Bridge': VerseType.tags[VerseType.Bridge], 'bridge': VerseType.tags[VerseType.Bridge],
'Interlude': VerseType.tags[VerseType.Bridge], 'interlude': VerseType.tags[VerseType.Bridge],
'Zwischenspiel': VerseType.tags[VerseType.Bridge], 'zwischenspiel': VerseType.tags[VerseType.Bridge],
'Pre-Chorus': VerseType.tags[VerseType.PreChorus], 'pre-chorus': VerseType.tags[VerseType.PreChorus],
'Pre-Refrain': VerseType.tags[VerseType.PreChorus], 'pre-refrain': VerseType.tags[VerseType.PreChorus],
'Misc': VerseType.tags[VerseType.Other], 'misc': VerseType.tags[VerseType.Other],
'Pre-Bridge': VerseType.tags[VerseType.Other], 'pre-bridge': VerseType.tags[VerseType.Other],
'Pre-Coda': VerseType.tags[VerseType.Other], 'pre-coda': VerseType.tags[VerseType.Other],
'Part': VerseType.tags[VerseType.Other], 'part': VerseType.tags[VerseType.Other],
'Teil': VerseType.tags[VerseType.Other], 'teil': VerseType.tags[VerseType.Other],
'Unbekannt': VerseType.tags[VerseType.Other], 'unbekannt': VerseType.tags[VerseType.Other],
'Unknown': VerseType.tags[VerseType.Other], 'unknown': VerseType.tags[VerseType.Other],
'Unbenannt': VerseType.tags[VerseType.Other], 'unbenannt': VerseType.tags[VerseType.Other],
'$$M=': VerseType.tags[VerseType.Other] '$$m=': VerseType.tags[VerseType.Other]
} }
@ -267,20 +267,20 @@ class SongBeamerImport(SongImport):
def check_verse_marks(self, line): def check_verse_marks(self, line):
""" """
Check and add the verse's MarkType. Returns ``True`` if the given linE contains a correct verse mark otherwise Check and add the verse's MarkType. Returns ``True`` if the given line contains a correct verse mark otherwise
``False``. ``False``.
:param line: The line to check for marks (unicode). :param line: The line to check for marks (unicode).
""" """
marks = line.split(' ') marks = line.split(' ')
if len(marks) <= 2 and marks[0] in SongBeamerTypes.MarkTypes: if len(marks) <= 2 and marks[0].lower() in SongBeamerTypes.MarkTypes:
self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0]] self.current_verse_type = SongBeamerTypes.MarkTypes[marks[0].lower()]
if len(marks) == 2: if len(marks) == 2:
# If we have a digit, we append it to current_verse_type. # If we have a digit, we append it to current_verse_type.
if marks[1].isdigit(): if marks[1].isdigit():
self.current_verse_type += marks[1] self.current_verse_type += marks[1]
return True return True
elif marks[0].startswith('$$M='): # this verse-mark cannot be numbered elif marks[0].lower().startswith('$$m='): # this verse-mark cannot be numbered
self.current_verse_type = SongBeamerTypes.MarkTypes['$$M='] self.current_verse_type = SongBeamerTypes.MarkTypes['$$m=']
return True return True
return False return False

View File

@ -211,7 +211,8 @@ class SongsPlugin(Plugin):
if self.media_item: if self.media_item:
self.media_item.on_export_click() self.media_item.on_export_click()
def about(self): @staticmethod
def about():
""" """
Provides information for the plugin manager to display. Provides information for the plugin manager to display.
@ -296,7 +297,7 @@ class SongsPlugin(Plugin):
if sfile.startswith('songs_') and sfile.endswith('.sqlite'): if sfile.startswith('songs_') and sfile.endswith('.sqlite'):
self.application.process_events() self.application.process_events()
song_dbs.append(os.path.join(db_dir, sfile)) song_dbs.append(os.path.join(db_dir, sfile))
song_count += self._count_songs(os.path.join(db_dir, sfile)) song_count += SongsPlugin._count_songs(os.path.join(db_dir, sfile))
if not song_dbs: if not song_dbs:
return return
self.application.process_events() self.application.process_events()
@ -343,7 +344,8 @@ class SongsPlugin(Plugin):
for song in songs: for song in songs:
self.manager.delete_object(Song, song.id) self.manager.delete_object(Song, song.id)
def _count_songs(self, db_file): @staticmethod
def _count_songs(db_file):
""" """
Provide a count of the songs in the database Provide a count of the songs in the database

View File

@ -36,8 +36,8 @@ class SongUsageDeleteForm(QtWidgets.QDialog, Ui_SongUsageDeleteDialog, RegistryP
Constructor Constructor
""" """
self.manager = manager self.manager = manager
super(SongUsageDeleteForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SongUsageDeleteForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint |
| QtCore.Qt.WindowTitleHint) QtCore.Qt.WindowTitleHint)
self.setupUi(self) self.setupUi(self)
self.button_box.clicked.connect(self.on_button_box_clicked) self.button_box.clicked.connect(self.on_button_box_clicked)
@ -48,12 +48,13 @@ class SongUsageDeleteForm(QtWidgets.QDialog, Ui_SongUsageDeleteDialog, RegistryP
:param button: The button pressed :param button: The button pressed
""" """
if self.button_box.standardButton(button) == QtWidgets.QDialogButtonBox.Ok: if self.button_box.standardButton(button) == QtWidgets.QDialogButtonBox.Ok:
ret = QtWidgets.QMessageBox.question( ret = QtWidgets.QMessageBox.question(self,
self, translate('SongUsagePlugin.SongUsageDeleteForm',
translate('SongUsagePlugin.SongUsageDeleteForm', 'Delete Selected Song Usage Events?'), 'Delete Selected Song Usage Events?'),
translate('SongUsagePlugin.SongUsageDeleteForm', translate('SongUsagePlugin.SongUsageDeleteForm',
'Are you sure you want to delete selected Song Usage data?'), 'Are you sure you want to delete selected Song Usage data?'),
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No), QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No),
QtWidgets.QMessageBox.No) QtWidgets.QMessageBox.No)
if ret == QtWidgets.QMessageBox.Yes: if ret == QtWidgets.QMessageBox.Yes:
delete_date = self.delete_calendar.selectedDate().toPyDate() delete_date = self.delete_calendar.selectedDate().toPyDate()

View File

@ -44,8 +44,7 @@ class SongUsageDetailForm(QtWidgets.QDialog, Ui_SongUsageDetailDialog, RegistryP
""" """
Initialise the form Initialise the form
""" """
super(SongUsageDetailForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint super(SongUsageDetailForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
| QtCore.Qt.WindowTitleHint)
self.plugin = plugin self.plugin = plugin
self.setupUi(self) self.setupUi(self)

View File

@ -227,7 +227,8 @@ class SongUsagePlugin(Plugin):
self.song_usage_detail_form.initialise() self.song_usage_detail_form.initialise()
self.song_usage_detail_form.exec() self.song_usage_detail_form.exec()
def about(self): @staticmethod
def about():
""" """
The plugin about text The plugin about text

View File

@ -25,10 +25,9 @@ Package to test the openlp.core.lib.projector.pjlink1 package.
from unittest import TestCase from unittest import TestCase
from mock import MagicMock, patch
from openlp.core.lib.projector.pjlink1 import PJLink1 from openlp.core.lib.projector.pjlink1 import PJLink1
from tests.functional import patch
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE
pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True) pjlink_test = PJLink1(name='test', ip='127.0.0.1', pin=TEST_PIN, no_poll=True)

View File

@ -142,12 +142,10 @@ class TestMainDisplay(TestCase, TestMixin):
mocked_bibles_plugin.refresh_css.assert_called_with(main_display.frame) mocked_bibles_plugin.refresh_css.assert_called_with(main_display.frame)
@skipUnless(is_macosx(), 'Can only run test on Mac OS X due to pyobjc dependency.') @skipUnless(is_macosx(), 'Can only run test on Mac OS X due to pyobjc dependency.')
def macosx_display_window_flags_state_test(self, is_macosx): def macosx_display_window_flags_state_test(self):
""" """
Test that on Mac OS X we set the proper window flags Test that on Mac OS X we set the proper window flags
""" """
if not is_macosx():
self.skipTest('Can only run test on Mac OS X due to pyobjc dependency.')
# GIVEN: A new SlideController instance on Mac OS X. # GIVEN: A new SlideController instance on Mac OS X.
self.screens.set_current_display(0) self.screens.set_current_display(0)
display = MagicMock() display = MagicMock()

View File

@ -40,7 +40,7 @@ class MediaPluginTest(TestCase, TestMixin):
@patch(u'openlp.plugins.media.mediaplugin.Plugin.initialise') @patch(u'openlp.plugins.media.mediaplugin.Plugin.initialise')
@patch(u'openlp.plugins.media.mediaplugin.Settings') @patch(u'openlp.plugins.media.mediaplugin.Settings')
def initialise_test(self, MockedSettings, mocked_initialise): def initialise_test(self, _mocked_settings, mocked_initialise):
""" """
Test that the initialise() method overwrites the built-in one, but still calls it Test that the initialise() method overwrites the built-in one, but still calls it
""" """
@ -48,7 +48,7 @@ class MediaPluginTest(TestCase, TestMixin):
media_plugin = MediaPlugin() media_plugin = MediaPlugin()
mocked_settings = MagicMock() mocked_settings = MagicMock()
mocked_settings.get_files_from_config.return_value = True # Not the real value, just need something "true-ish" mocked_settings.get_files_from_config.return_value = True # Not the real value, just need something "true-ish"
MockedSettings.return_value = mocked_settings _mocked_settings.return_value = mocked_settings
# WHEN: initialise() is called # WHEN: initialise() is called
media_plugin.initialise() media_plugin.initialise()
@ -57,3 +57,11 @@ class MediaPluginTest(TestCase, TestMixin):
mocked_settings.get_files_from_config.assert_called_with(media_plugin) mocked_settings.get_files_from_config.assert_called_with(media_plugin)
mocked_settings.setValue.assert_called_with('media/media files', True) mocked_settings.setValue.assert_called_with('media/media files', True)
mocked_initialise.assert_called_with() mocked_initialise.assert_called_with()
def test_about_text(self):
# GIVEN: The MediaPlugin
# WHEN: Retrieving the about text
# THEN: about() should return a string object
self.assertIsInstance(MediaPlugin.about(), str)
# THEN: about() should return a non-empty string
self.assertNotEquals(len(MediaPlugin.about()), 0)

View File

@ -39,7 +39,7 @@ class TestFoilPresenter(TestCase):
""" """
# TODO: The following modules still need tests written for # TODO: The following modules still need tests written for
# xml_to_song # xml_to_song
# _child # to_str
# _process_authors # _process_authors
# _process_cclinumber # _process_cclinumber
# _process_comments # _process_comments
@ -50,7 +50,7 @@ class TestFoilPresenter(TestCase):
# _process_topics # _process_topics
def setUp(self): def setUp(self):
self.child_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.FoilPresenter._child') self.to_str_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.to_str')
self.clean_song_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.clean_song') self.clean_song_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.clean_song')
self.objectify_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.objectify') self.objectify_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.objectify')
self.process_authors_patcher = \ self.process_authors_patcher = \
@ -72,7 +72,7 @@ class TestFoilPresenter(TestCase):
self.song_xml_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.SongXML') self.song_xml_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.SongXML')
self.translate_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.translate') self.translate_patcher = patch('openlp.plugins.songs.lib.importers.foilpresenter.translate')
self.mocked_child = self.child_patcher.start() self.mocked_child = self.to_str_patcher.start()
self.mocked_clean_song = self.clean_song_patcher.start() self.mocked_clean_song = self.clean_song_patcher.start()
self.mocked_objectify = self.objectify_patcher.start() self.mocked_objectify = self.objectify_patcher.start()
self.mocked_process_authors = self.process_authors_patcher.start() self.mocked_process_authors = self.process_authors_patcher.start()
@ -92,7 +92,7 @@ class TestFoilPresenter(TestCase):
self.mocked_song_import = MagicMock() self.mocked_song_import = MagicMock()
def tearDown(self): def tearDown(self):
self.child_patcher.stop() self.to_str_patcher.stop()
self.clean_song_patcher.stop() self.clean_song_patcher.stop()
self.objectify_patcher.stop() self.objectify_patcher.stop()
self.process_authors_patcher.stop() self.process_authors_patcher.stop()

View File

@ -22,10 +22,13 @@
""" """
This module contains tests for the OpenOffice/LibreOffice importer. This module contains tests for the OpenOffice/LibreOffice importer.
""" """
from unittest import TestCase from unittest import TestCase, SkipTest
from openlp.core.common import Registry from openlp.core.common import Registry
try:
from openlp.plugins.songs.lib.importers.openoffice import OpenOfficeImport from openlp.plugins.songs.lib.importers.openoffice import OpenOfficeImport
except ImportError:
raise SkipTest('Could not import OpenOfficeImport probably due to unavailability of uno')
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch
from tests.helpers.testmixin import TestMixin from tests.helpers.testmixin import TestMixin

View File

@ -28,7 +28,7 @@ from unittest import TestCase
from tests.helpers.songfileimport import SongImportTestHelper from tests.helpers.songfileimport import SongImportTestHelper
from tests.functional import MagicMock, patch from tests.functional import MagicMock, patch
from openlp.plugins.songs.lib.importers.songbeamer import SongBeamerImport from openlp.plugins.songs.lib.importers.songbeamer import SongBeamerImport, SongBeamerTypes
from openlp.core.common import Registry from openlp.core.common import Registry
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__),
@ -131,22 +131,22 @@ class TestSongBeamerImport(TestCase):
self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>') self.assertEqual(self.current_verse_type, 'c', '<Refrain> should be interpreted as <c>')
# GIVEN: line with unnumbered verse-type and trailing space # GIVEN: line with unnumbered verse-type and trailing space
line = 'Refrain ' line = 'ReFrain '
self.current_verse_type = None self.current_verse_type = None
# WHEN: line is being checked for verse marks # WHEN: line is being checked for verse marks
result = SongBeamerImport.check_verse_marks(self, line) result = SongBeamerImport.check_verse_marks(self, line)
# THEN: we should get back true and c as self.current_verse_type # THEN: we should get back true and c as self.current_verse_type
self.assertTrue(result, 'Versemark for <Refrain > should be found, value true') self.assertTrue(result, 'Versemark for <ReFrain > should be found, value true')
self.assertEqual(self.current_verse_type, 'c', '<Refrain > should be interpreted as <c>') self.assertEqual(self.current_verse_type, 'c', '<ReFrain > should be interpreted as <c>')
# GIVEN: line with numbered verse-type # GIVEN: line with numbered verse-type
line = 'Verse 1' line = 'VersE 1'
self.current_verse_type = None self.current_verse_type = None
# WHEN: line is being checked for verse marks # WHEN: line is being checked for verse marks
result = SongBeamerImport.check_verse_marks(self, line) result = SongBeamerImport.check_verse_marks(self, line)
# THEN: we should get back true and v1 as self.current_verse_type # THEN: we should get back true and v1 as self.current_verse_type
self.assertTrue(result, 'Versemark for <Verse 1> should be found, value true') self.assertTrue(result, 'Versemark for <VersE 1> should be found, value true')
self.assertEqual(self.current_verse_type, 'v1', u'<Verse 1> should be interpreted as <v1>') self.assertEqual(self.current_verse_type, 'v1', u'<VersE 1> should be interpreted as <v1>')
# GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags) # GIVEN: line with special unnumbered verse-mark (used in Songbeamer to allow usage of non-supported tags)
line = '$$M=special' line = '$$M=special'
@ -192,3 +192,12 @@ class TestSongBeamerImport(TestCase):
# THEN: we should get back false and none as self.current_verse_type # THEN: we should get back false and none as self.current_verse_type
self.assertFalse(result, 'No versemark for <> should be found, value false') self.assertFalse(result, 'No versemark for <> should be found, value false')
self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark') self.assertIsNone(self.current_verse_type, '<> should be interpreted as none versemark')
def test_verse_marks_defined_in_lowercase(self):
"""
Test that the verse marks are all defined in lowercase
"""
# GIVEN: SongBeamber MarkTypes
for tag in SongBeamerTypes.MarkTypes.keys():
# THEN: tag should be defined in lowercase
self.assertEquals(tag, tag.lower(), 'Tags should be defined in lowercase')

View File

@ -80,4 +80,16 @@ class TestSongFormat(TestCase):
for song_format in SongFormat.get_format_list(): for song_format in SongFormat.get_format_list():
# THEN: Return all attributes that were specified # THEN: Return all attributes that were specified
self.assertEquals(len(SongFormat.get(song_format, 'canDisable', 'availability')), 2, self.assertEquals(len(SongFormat.get(song_format, 'canDisable', 'availability')), 2,
"Did not return the correct number of attributes when retrieving multiple attributes at once") "Did not return the correct number of attributes"
" when retrieving multiple attributes at once")
def test_get_format_list_returns_ordered_list(self):
"""
Test that get_format_list() returns a list that is ordered
according to the order specified in SongFormat
"""
# GIVEN: The SongFormat class
# WHEN: Retrieving all formats
# THEN: The returned list should be sorted according to the ordering defined in SongFormat
self.assertEquals(sorted(SongFormat.get_format_list()), SongFormat.get_format_list(),
"The list returned should be sorted according to the ordering in SongFormat")

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2016 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 #
###############################################################################
"""
Tests for the Songusage plugin
"""

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2016 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 module contains tests for the Songusage plugin.
"""
from unittest import TestCase
from openlp.plugins.songusage.songusageplugin import SongUsagePlugin
class TestSongUsage(TestCase):
def test_about_text(self):
# GIVEN: The SongUsagePlugin
# WHEN: Retrieving the about text
# THEN: about() should return a string object
self.assertIsInstance(SongUsagePlugin.about(), str)
# THEN: about() should return a non-empty string
self.assertNotEquals(len(SongUsagePlugin.about()), 0)
self.assertNotEquals(len(SongUsagePlugin.about()), 0)

View File

@ -29,7 +29,7 @@ from subprocess import Popen, PIPE
TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10', TAGS1 = {'1.9.0', '1.9.1', '1.9.2', '1.9.3', '1.9.4', '1.9.5', '1.9.6', '1.9.7', '1.9.8', '1.9.9', '1.9.10',
'1.9.11', '1.9.12', '2.0', '2.1.0', '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.2', '1.9.11', '1.9.12', '2.0', '2.1.0', '2.1.1', '2.1.2', '2.1.3', '2.1.4', '2.1.5', '2.1.6', '2.2',
'2.3.1'} '2.3.1', '2.3.2'}
class TestBzrTags(TestCase): class TestBzrTags(TestCase):