Merged changes from trunk

This commit is contained in:
Ian Knight 2016-03-21 01:30:15 +10:30
commit 80a8abf4d0
19 changed files with 532 additions and 71 deletions

View File

@ -132,6 +132,7 @@ class Settings(QtCore.QSettings):
'advanced/save current plugin': False,
'advanced/slide limits': SlideLimits.End,
'advanced/single click preview': False,
'advanced/single click service preview': False,
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
'advanced/search as type': True,
'crashreport/last directory': '',

View File

@ -77,6 +77,9 @@ class AdvancedTab(SettingsTab):
self.single_click_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.single_click_preview_check_box.setObjectName('single_click_preview_check_box')
self.ui_layout.addRow(self.single_click_preview_check_box)
self.single_click_service_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box)
self.single_click_service_preview_check_box.setObjectName('single_click_service_preview_check_box')
self.ui_layout.addRow(self.single_click_service_preview_check_box)
self.expand_service_item_check_box = QtWidgets.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)
@ -277,6 +280,8 @@ class AdvancedTab(SettingsTab):
'Double-click to send items straight to live'))
self.single_click_preview_check_box.setText(translate('OpenLP.AdvancedTab',
'Preview items when clicked in Media Manager'))
self.single_click_service_preview_check_box.setText(translate('OpenLP.AdvancedTab',
'Preview items when clicked in Service Manager'))
self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab',
'Expand new service items on creation'))
self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab',
@ -349,6 +354,7 @@ class AdvancedTab(SettingsTab):
self.media_plugin_check_box.setChecked(settings.value('save current plugin'))
self.double_click_live_check_box.setChecked(settings.value('double click live'))
self.single_click_preview_check_box.setChecked(settings.value('single click preview'))
self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview'))
self.expand_service_item_check_box.setChecked(settings.value('expand service item'))
self.slide_max_height_spin_box.setValue(settings.value('slide max height'))
self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation'))
@ -431,6 +437,7 @@ class AdvancedTab(SettingsTab):
settings.setValue('save current plugin', self.media_plugin_check_box.isChecked())
settings.setValue('double click live', self.double_click_live_check_box.isChecked())
settings.setValue('single click preview', self.single_click_preview_check_box.isChecked())
settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked())
settings.setValue('expand service item', self.expand_service_item_check_box.isChecked())
settings.setValue('slide max height', self.slide_max_height_spin_box.value())
settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked())

View File

@ -180,11 +180,13 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties):
if ':' in line:
exception = line.split('\n')[-1].split(':')[0]
subject = 'Bug report: %s in %s' % (exception, source)
mail_to_url = QtCore.QUrlQuery('mailto:bugs@openlp.org')
mail_to_url.addQueryItem('subject', subject)
mail_to_url.addQueryItem('body', self.report_text % content)
mail_urlquery = QtCore.QUrlQuery()
mail_urlquery.addQueryItem('subject', subject)
mail_urlquery.addQueryItem('body', self.report_text % content)
if self.file_attachment:
mail_to_url.addQueryItem('attach', self.file_attachment)
mail_urlquery.addQueryItem('attach', self.file_attachment)
mail_to_url = QtCore.QUrl('mailto:bugs@openlp.org')
mail_to_url.setQuery(mail_urlquery)
QtGui.QDesktopServices.openUrl(mail_to_url)
def on_description_updated(self):

View File

@ -408,10 +408,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
self.application.process_events()
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
fade_shake_timer.stop()
elif is_win():
self.shake_web_view()
# Wait for the webview to update before getting the preview.
# Important otherwise first preview will miss the background !
while not self.web_loaded:
@ -429,6 +426,9 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
self.setVisible(True)
else:
self.setVisible(True)
# Workaround for bug #1531319, should not be needed with PyQt 5.6.
if is_win():
self.shake_web_view()
return self.grab()
def build_html(self, service_item, image_path=''):

View File

@ -83,60 +83,60 @@ class Ui_ProjectorManager(object):
self.one_toolbar.add_toolbar_action('new_projector',
text=translate('OpenLP.ProjectorManager', 'Add Projector'),
icon=':/projector/projector_new.png',
tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'),
tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector.'),
triggers=self.on_add_projector)
# Show edit/delete when projector not connected
self.one_toolbar.add_toolbar_action('edit_projector',
text=translate('OpenLP.ProjectorManager', 'Edit Projector'),
icon=':/general/general_edit.png',
tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector'),
tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector.'),
triggers=self.on_edit_projector)
self.one_toolbar.add_toolbar_action('delete_projector',
text=translate('OpenLP.ProjectorManager', 'Delete Projector'),
icon=':/general/general_delete.png',
tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector'),
tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector.'),
triggers=self.on_delete_projector)
# Show source/view when projector connected
self.one_toolbar.add_toolbar_action('source_view_projector',
text=translate('OpenLP.ProjectorManager', 'Select Input Source'),
icon=':/projector/projector_hdmi.png',
tooltip=translate('OpenLP.ProjectorManager',
'Choose input source on selected projector'),
'Choose input source on selected projector.'),
triggers=self.on_select_input)
self.one_toolbar.add_toolbar_action('view_projector',
text=translate('OpenLP.ProjectorManager', 'View Projector'),
icon=':/system/system_about.png',
tooltip=translate('OpenLP.ProjectorManager',
'View selected projector information'),
'View selected projector information.'),
triggers=self.on_status_projector)
self.one_toolbar.addSeparator()
self.one_toolbar.add_toolbar_action('connect_projector',
text=translate('OpenLP.ProjectorManager',
'Connect to selected projector'),
'Connect to selected projector.'),
icon=':/projector/projector_connect.png',
tooltip=translate('OpenLP.ProjectorManager',
'Connect to selected projector'),
'Connect to selected projector.'),
triggers=self.on_connect_projector)
self.one_toolbar.add_toolbar_action('connect_projector_multiple',
text=translate('OpenLP.ProjectorManager',
'Connect to selected projectors'),
icon=':/projector/projector_connect_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Connect to selected projector'),
'Connect to selected projectors.'),
triggers=self.on_connect_projector)
self.one_toolbar.add_toolbar_action('disconnect_projector',
text=translate('OpenLP.ProjectorManager',
'Disconnect from selected projectors'),
icon=':/projector/projector_disconnect.png',
tooltip=translate('OpenLP.ProjectorManager',
'Disconnect from selected projector'),
'Disconnect from selected projector.'),
triggers=self.on_disconnect_projector)
self.one_toolbar.add_toolbar_action('disconnect_projector_multiple',
text=translate('OpenLP.ProjectorManager',
'Disconnect from selected projector'),
icon=':/projector/projector_disconnect_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Disconnect from selected projector'),
'Disconnect from selected projectors.'),
triggers=self.on_disconnect_projector)
self.one_toolbar.addSeparator()
self.one_toolbar.add_toolbar_action('poweron_projector',
@ -144,26 +144,26 @@ class Ui_ProjectorManager(object):
'Power on selected projector'),
icon=':/projector/projector_power_on.png',
tooltip=translate('OpenLP.ProjectorManager',
'Power on selected projector'),
'Power on selected projector.'),
triggers=self.on_poweron_projector)
self.one_toolbar.add_toolbar_action('poweron_projector_multiple',
text=translate('OpenLP.ProjectorManager',
'Power on selected projector'),
icon=':/projector/projector_power_on_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Power on selected projector'),
'Power on selected projectors.'),
triggers=self.on_poweron_projector)
self.one_toolbar.add_toolbar_action('poweroff_projector',
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
icon=':/projector/projector_power_off.png',
tooltip=translate('OpenLP.ProjectorManager',
'Put selected projector in standby'),
'Put selected projector in standby.'),
triggers=self.on_poweroff_projector)
self.one_toolbar.add_toolbar_action('poweroff_projector_multiple',
text=translate('OpenLP.ProjectorManager', 'Standby selected projector'),
icon=':/projector/projector_power_off_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Put selected projector in standby'),
'Put selected projectors in standby.'),
triggers=self.on_poweroff_projector)
self.one_toolbar.addSeparator()
self.one_toolbar.add_toolbar_action('blank_projector',
@ -175,24 +175,24 @@ class Ui_ProjectorManager(object):
triggers=self.on_blank_projector)
self.one_toolbar.add_toolbar_action('blank_projector_multiple',
text=translate('OpenLP.ProjectorManager',
'Blank selected projector screen'),
'Blank selected projectors screen'),
icon=':/projector/projector_blank_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Blank selected projector screen'),
'Blank selected projectors screen.'),
triggers=self.on_blank_projector)
self.one_toolbar.add_toolbar_action('show_projector',
text=translate('OpenLP.ProjectorManager',
'Show selected projector screen'),
icon=':/projector/projector_show.png',
tooltip=translate('OpenLP.ProjectorManager',
'Show selected projector screen'),
'Show selected projector screen.'),
triggers=self.on_show_projector)
self.one_toolbar.add_toolbar_action('show_projector_multiple',
text=translate('OpenLP.ProjectorManager',
'Show selected projector screen'),
icon=':/projector/projector_show_tiled.png',
tooltip=translate('OpenLP.ProjectorManager',
'Show selected projector screen'),
'Show selected projectors screen.'),
triggers=self.on_show_projector)
self.layout.addWidget(self.one_toolbar)
self.projector_one_widget = QtWidgets.QWidgetAction(self.one_toolbar)

View File

@ -211,7 +211,8 @@ class Ui_ServiceManager(object):
self.layout.addWidget(self.order_toolbar)
# Connect up our signals and slots
self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected)
self.service_manager_list.doubleClicked.connect(self.on_make_live)
self.service_manager_list.doubleClicked.connect(self.on_double_click_live)
self.service_manager_list.clicked.connect(self.on_single_click_preview)
self.service_manager_list.itemCollapsed.connect(self.collapsed)
self.service_manager_list.itemExpanded.connect(self.expanded)
# Last little bits of setting up
@ -319,6 +320,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
self._modified = False
self._file_name = ''
self.service_has_all_original_files = True
self.list_double_clicked = False
def bootstrap_initialise(self):
"""
@ -1454,13 +1456,38 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
else:
return self.service_items[item]['service_item']
def on_make_live(self, field=None):
def on_double_click_live(self, field=None):
"""
Send the current item to the Live slide controller but triggered by a tablewidget click event.
:param field:
"""
self.list_double_clicked = True
self.make_live()
def on_single_click_preview(self, field=None):
"""
If single click previewing is enabled, and triggered by a tablewidget click event,
start a timeout to verify a double-click hasn't triggered.
:param field:
"""
if Settings().value('advanced/single click service preview'):
if not self.list_double_clicked:
# If a double click has not registered start a timer, otherwise wait for the existing timer to finish.
QtCore.QTimer.singleShot(QtWidgets.QApplication.instance().doubleClickInterval(),
self.on_single_click_preview_timeout)
def on_single_click_preview_timeout(self):
"""
If a single click ok, but double click not triggered, send the current item to the Preview slide controller.
:param field:
"""
if self.list_double_clicked:
# If a double click has registered, clear it.
self.list_double_clicked = False
else:
# Otherwise preview the item.
self.make_preview()
def make_live(self, row=-1):
"""
Send the current item to the Live slide controller

View File

@ -198,6 +198,7 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog):
# Insert all slides to make the old_slides list complete.
for slide in slides:
old_slides.insert(old_row, slide)
old_row += 1
self.slide_list_view.addItems(old_slides)
self.slide_list_view.repaint()

View File

@ -289,40 +289,45 @@ class EasyWorshipSongImport(SongImport):
for i in range(rec_count):
if self.stop_import_flag:
break
raw_record = db_file.read(record_size)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
self.title = self.get_field(fi_title).decode(self.encoding)
# Get remaining fields.
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
if copy:
self.copyright = copy.decode(self.encoding)
if admin:
try:
raw_record = db_file.read(record_size)
self.fields = self.record_structure.unpack(raw_record)
self.set_defaults()
self.title = self.get_field(fi_title).decode(self.encoding)
# Get remaining fields.
copy = self.get_field(fi_copy)
admin = self.get_field(fi_admin)
ccli = self.get_field(fi_ccli)
authors = self.get_field(fi_author)
words = self.get_field(fi_words)
if copy:
self.copyright += ', '
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
'Administered by %s') % admin.decode(self.encoding)
if ccli:
self.ccli_number = ccli.decode(self.encoding)
if authors:
authors = authors.decode(self.encoding)
else:
authors = ''
# Set the SongImport object members.
self.set_song_import_object(authors, words)
if self.stop_import_flag:
break
if self.entry_error_log:
self.copyright = copy.decode(self.encoding)
if admin:
if copy:
self.copyright += ', '
self.copyright += translate('SongsPlugin.EasyWorshipSongImport',
'Administered by %s') % admin.decode(self.encoding)
if ccli:
self.ccli_number = ccli.decode(self.encoding)
if authors:
authors = authors.decode(self.encoding)
else:
authors = ''
# Set the SongImport object members.
self.set_song_import_object(authors, words)
if self.stop_import_flag:
break
if self.entry_error_log:
self.log_error(self.import_source,
translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
% (self.title, self.entry_error_log))
self.entry_error_log = ''
elif not self.finish():
self.log_error(self.import_source)
except Exception as e:
self.log_error(self.import_source,
translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s')
% (self.title, self.entry_error_log))
self.entry_error_log = ''
elif not self.finish():
self.log_error(self.import_source)
% (self.title, e))
db_file.close()
self.memo_file.close()
@ -368,7 +373,7 @@ class EasyWorshipSongImport(SongImport):
first_line_is_tag = False
# EW tags: verse, chorus, pre-chorus, bridge, tag,
# intro, ending, slide
for tag in VerseType.tags + ['tag', 'slide']:
for tag in VerseType.names + ['tag', 'slide', 'end']:
tag = tag.lower()
ew_tag = verse_split[0].strip().lower()
if ew_tag.startswith(tag):
@ -390,6 +395,9 @@ class EasyWorshipSongImport(SongImport):
if not number_found:
verse_type += '1'
break
# If the verse only consist of the tag-line, add an empty line to create an empty slide
if first_line_is_tag and len(verse_split) == 1:
verse_split.append("")
self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type)
if len(self.comments) > 5:
self.comments += str(translate('SongsPlugin.EasyWorshipSongImport',

View File

@ -26,7 +26,7 @@ import os
import shutil
from PyQt5 import QtCore, QtWidgets
from sqlalchemy.sql import or_
from sqlalchemy.sql import and_, or_
from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \
@ -37,7 +37,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry
from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, SongBookEntry, Topic
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML
@ -52,8 +52,11 @@ class SongSearch(object):
Titles = 2
Lyrics = 3
Authors = 4
Books = 5
Themes = 6
Topics = 5
Books = 6
Themes = 7
Copyright = 8
CCLInumber = 9
class SongMediaItem(MediaManagerItem):
@ -151,9 +154,17 @@ class SongMediaItem(MediaManagerItem):
translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
(SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
translate('SongsPlugin.MediaItem', 'Search Authors...')),
(SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics,
translate('SongsPlugin.MediaItem', 'Search Topics...')),
(SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
translate('SongsPlugin.MediaItem', 'Search Songbooks...')),
(SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes)
(SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes),
(SongSearch.Copyright, ':/songs/song_search_copy.png',
translate('SongsPlugin.MediaItem', 'Copyright'),
translate('SongsPlugin.MediaItem', 'Search Copyright...')),
(SongSearch.CCLInumber, ':/songs/song_search_ccli.png',
translate('SongsPlugin.MediaItem', 'CCLI number'),
translate('SongsPlugin.MediaItem', 'Search CCLI number...'))
])
self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
self.config_update()
@ -184,14 +195,33 @@ class SongMediaItem(MediaManagerItem):
search_results = self.plugin.manager.get_all_objects(
Author, Author.display_name.like(search_string), Author.display_name.asc())
self.display_results_author(search_results)
elif search_type == SongSearch.Topics:
log.debug('Topics Search')
search_string = '%' + search_keywords + '%'
search_results = self.plugin.manager.get_all_objects(
Topic, Topic.name.like(search_string), Topic.name.asc())
self.display_results_topic(search_results)
elif search_type == SongSearch.Books:
log.debug('Songbook Search')
self.display_results_book(search_keywords)
elif search_type == SongSearch.Themes:
log.debug('Theme Search')
search_string = '%' + search_keywords + '%'
search_results = self.plugin.manager.get_all_objects(Song, Song.theme_name.like(search_string))
search_results = self.plugin.manager.get_all_objects(
Song, Song.theme_name.like(search_string), Song.theme_name.asc())
self.display_results_themes(search_results)
elif search_type == SongSearch.Copyright:
log.debug('Copyright Search')
search_string = '%' + search_keywords + '%'
search_results = self.plugin.manager.get_all_objects(
Song, and_(Song.copyright.like(search_string), Song.copyright != ''))
self.display_results_song(search_results)
elif search_type == SongSearch.CCLInumber:
log.debug('CCLI number Search')
search_string = '%' + search_keywords + '%'
search_results = self.plugin.manager.get_all_objects(
Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''))
self.display_results_cclinumber(search_results)
self.check_search_result()
def search_entire(self, search_keywords):
@ -215,6 +245,12 @@ class SongMediaItem(MediaManagerItem):
log.debug('on_song_list_load - finished')
def display_results_song(self, search_results):
"""
Display the song search results in the media manager list
:param search_results: A list of db Song objects
:return: None
"""
log.debug('display results Song')
self.save_auto_select_id()
self.list_view.clear()
@ -234,6 +270,12 @@ class SongMediaItem(MediaManagerItem):
self.auto_select_id = -1
def display_results_author(self, search_results):
"""
Display the song search results in the media manager list, grouped by author
:param search_results: A list of db Author objects
:return: None
"""
log.debug('display results Author')
self.list_view.clear()
for author in search_results:
@ -247,6 +289,13 @@ class SongMediaItem(MediaManagerItem):
self.list_view.addItem(song_name)
def display_results_book(self, search_keywords):
"""
Display the song search results in the media manager list, grouped by book
:param search_keywords: A list of search keywords - book first, then number
:return: None
"""
log.debug('display results Book')
self.list_view.clear()
@ -270,6 +319,64 @@ class SongMediaItem(MediaManagerItem):
song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id)
self.list_view.addItem(song_name)
def display_results_topic(self, search_results):
"""
Display the song search results in the media manager list, grouped by topic
:param search_results: A list of db Topic objects
:return: None
"""
log.debug('display results Topic')
self.list_view.clear()
search_results = sorted(search_results, key=lambda topic: self._natural_sort_key(topic.name))
for topic in search_results:
songs = sorted(topic.songs, key=lambda song: song.sort_key)
for song in songs:
# Do not display temporary songs
if song.temporary:
continue
song_detail = '%s (%s)' % (topic.name, song.title)
song_name = QtWidgets.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)
def display_results_themes(self, search_results):
"""
Display the song search results in the media manager list, sorted by theme
:param search_results: A list of db Song objects
:return: None
"""
log.debug('display results Themes')
self.list_view.clear()
for song in search_results:
# Do not display temporary songs
if song.temporary:
continue
song_detail = '%s (%s)' % (song.theme_name, song.title)
song_name = QtWidgets.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)
def display_results_cclinumber(self, search_results):
"""
Display the song search results in the media manager list, sorted by CCLI number
:param search_results: A list of db Song objects
:return: None
"""
log.debug('display results CCLI number')
self.list_view.clear()
songs = sorted(search_results, key=lambda song: self._natural_sort_key(song.ccli_number))
for song in songs:
# Do not display temporary songs
if song.temporary:
continue
song_detail = '%s (%s)' % (song.ccli_number, song.title)
song_name = QtWidgets.QListWidgetItem(song_detail)
song_name.setData(QtCore.Qt.UserRole, song.id)
self.list_view.addItem(song_name)
def on_clear_text_button_click(self):
"""
Clear the search text.
@ -587,6 +694,14 @@ class SongMediaItem(MediaManagerItem):
# List must be empty at the end
return not author_list
def _natural_sort_key(self, s):
"""
Return a tuple by which s is sorted.
:param s: A string value from the list we want to sort.
"""
return [int(text) if text.isdecimal() else text.lower()
for text in re.split('(\d+)', s)]
def search(self, string, show_error):
"""
Search for some songs

View File

@ -3,8 +3,11 @@
<file>song_search_stop.png</file>
<file>song_search_all.png</file>
<file>song_search_author.png</file>
<file>song_search_ccli.png</file>
<file>song_search_copy.png</file>
<file>song_search_lyrics.png</file>
<file>song_search_title.png</file>
<file>song_search_topic.png</file>
<file>topic_edit.png</file>
<file>author_add.png</file>
<file>author_delete.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

View File

@ -26,6 +26,7 @@ Package to test the openlp.core.lib.projector.pjlink1 package.
from unittest import TestCase
from openlp.core.lib.projector.pjlink1 import PJLink1
from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING
from tests.functional import patch
from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE
@ -74,3 +75,20 @@ class TestPJLink(TestCase):
# THEN: Projector class should be set with proper value
self.assertEquals(pjlink.pjlink_class, '1',
'Non-standard class reply should have set proper class')
@patch.object(pjlink_test, 'change_status')
def status_change_test(self, mock_change_status):
"""
Test process_command call with ERR2 (Parameter) status
"""
# GIVEN: Test object
pjlink = pjlink_test
# WHEN: process_command is called with "ERR2" status from projector
pjlink.process_command('POWR', 'ERR2')
# THEN: change_status should have called change_status with E_UNDEFINED
# as first parameter
mock_change_status.called_with(E_PARAMETER,
'change_status should have been called with "{}"'.format(
ERROR_STRING[E_PARAMETER]))

View File

@ -22,13 +22,14 @@
"""
Package to test the openlp.core.ui.slidecontroller package.
"""
import PyQt5
from unittest import TestCase
from openlp.core.common import Registry, ThemeLevel
from openlp.core.common import Registry, ThemeLevel, Settings
from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities
from openlp.core.ui import ServiceManager
from tests.functional import MagicMock
from tests.functional import MagicMock, patch
class TestServiceManager(TestCase):
@ -540,3 +541,80 @@ class TestServiceManager(TestCase):
self.assertEquals(service_manager.timed_slide_interval.setChecked.call_count, 0, 'Should not be called')
self.assertEquals(service_manager.theme_menu.menuAction().setVisible.call_count, 1,
'Should have be called once')
@patch(u'openlp.core.ui.servicemanager.Settings')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def single_click_preview_test_true(self, mocked_singleShot, MockedSettings):
"""
Test that when "Preview items when clicked in Service Manager" enabled the preview timer starts
"""
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
mocked_settings = MagicMock()
mocked_settings.value.return_value = True
MockedSettings.return_value = mocked_settings
service_manager = ServiceManager(None)
# WHEN: on_single_click_preview() is called
service_manager.on_single_click_preview()
# THEN: timer should have been started
mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),
service_manager.on_single_click_preview_timeout)
@patch(u'openlp.core.ui.servicemanager.Settings')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
def single_click_preview_test_false(self, mocked_singleShot, MockedSettings):
"""
Test that when "Preview items when clicked in Service Manager" disabled the preview timer doesn't start
"""
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
mocked_settings = MagicMock()
mocked_settings.value.return_value = False
MockedSettings.return_value = mocked_settings
service_manager = ServiceManager(None)
# WHEN: on_single_click_preview() is called
service_manager.on_single_click_preview()
# THEN: timer should not be started
self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called')
@patch(u'openlp.core.ui.servicemanager.Settings')
@patch(u'PyQt5.QtCore.QTimer.singleShot')
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
def single_click_preview_test_double(self, mocked_make_live, mocked_singleShot, MockedSettings):
"""
Test that when a double click has registered the preview timer doesn't start
"""
# GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager.
mocked_settings = MagicMock()
mocked_settings.value.return_value = True
MockedSettings.return_value = mocked_settings
service_manager = ServiceManager(None)
# WHEN: on_single_click_preview() is called following a double click
service_manager.on_double_click_live()
service_manager.on_single_click_preview()
# THEN: timer should not be started
self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called')
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
def single_click_timeout_test_single(self, mocked_make_preview):
"""
Test that when a single click has been registered, the item is sent to preview
"""
# GIVEN: A service manager.
service_manager = ServiceManager(None)
# WHEN: on_single_click_preview() is called
service_manager.on_single_click_preview_timeout()
# THEN: make_preview() should have been called
self.assertEquals(mocked_make_preview.call_count, 1, 'Should have been called once')
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview')
@patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live')
def single_click_timeout_test_double(self, mocked_make_live, mocked_make_preview):
"""
Test that when a double click has been registered, the item does not goes to preview
"""
# GIVEN: A service manager.
service_manager = ServiceManager(None)
# WHEN: on_single_click_preview() is called after a double click
service_manager.on_double_click_live()
service_manager.on_single_click_preview_timeout()
# THEN: make_preview() should have been called
self.assertEquals(mocked_make_preview.call_count, 0, 'Should not be called')

View File

@ -48,6 +48,12 @@ class TestMediaItem(TestCase, TestMixin):
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
self.media_item = SongMediaItem(None, MagicMock())
self.media_item.save_auto_select_id = MagicMock()
self.media_item.list_view = MagicMock()
self.media_item.list_view.save_auto_select_id = MagicMock()
self.media_item.list_view.clear = MagicMock()
self.media_item.list_view.addItem = MagicMock()
self.media_item.auto_select_id = -1
self.media_item.display_songbook = False
self.media_item.display_copyright_symbol = False
self.setup_application()
@ -60,6 +66,151 @@ class TestMediaItem(TestCase, TestMixin):
"""
self.destroy_settings()
def display_results_song_test(self):
"""
Test displaying song search results with basic song
"""
# GIVEN: Search results, plus a mocked QtListWidgetItem
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
mock_search_results = []
mock_song = MagicMock()
mock_song.id = 1
mock_song.title = 'My Song'
mock_song.sort_key = 'My Song'
mock_song.authors = []
mock_author = MagicMock()
mock_author.display_name = 'My Author'
mock_song.authors.append(mock_author)
mock_song.temporary = False
mock_search_results.append(mock_song)
mock_qlist_widget = MagicMock()
MockedQListWidgetItem.return_value = mock_qlist_widget
# WHEN: I display song search results
self.media_item.display_results_song(mock_search_results)
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
self.media_item.list_view.clear.assert_called_with()
self.media_item.save_auto_select_id.assert_called_with()
MockedQListWidgetItem.assert_called_with('My Song (My Author)')
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
def display_results_author_test(self):
"""
Test displaying song search results grouped by author with basic song
"""
# GIVEN: Search results grouped by author, plus a mocked QtListWidgetItem
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
mock_search_results = []
mock_author = MagicMock()
mock_song = MagicMock()
mock_author.display_name = 'My Author'
mock_author.songs = []
mock_song.id = 1
mock_song.title = 'My Song'
mock_song.sort_key = 'My Song'
mock_song.temporary = False
mock_author.songs.append(mock_song)
mock_search_results.append(mock_author)
mock_qlist_widget = MagicMock()
MockedQListWidgetItem.return_value = mock_qlist_widget
# WHEN: I display song search results grouped by author
self.media_item.display_results_author(mock_search_results)
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
self.media_item.list_view.clear.assert_called_with()
MockedQListWidgetItem.assert_called_with('My Author (My Song)')
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
def display_results_topic_test(self):
"""
Test displaying song search results grouped by topic with basic song
"""
# GIVEN: Search results grouped by topic, plus a mocked QtListWidgetItem
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
mock_search_results = []
mock_topic = MagicMock()
mock_song = MagicMock()
mock_topic.name = 'My Topic'
mock_topic.songs = []
mock_song.id = 1
mock_song.title = 'My Song'
mock_song.sort_key = 'My Song'
mock_song.temporary = False
mock_topic.songs.append(mock_song)
mock_search_results.append(mock_topic)
mock_qlist_widget = MagicMock()
MockedQListWidgetItem.return_value = mock_qlist_widget
# WHEN: I display song search results grouped by topic
self.media_item.display_results_topic(mock_search_results)
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
self.media_item.list_view.clear.assert_called_with()
MockedQListWidgetItem.assert_called_with('My Topic (My Song)')
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
def display_results_themes_test(self):
"""
Test displaying song search results sorted by theme with basic song
"""
# GIVEN: Search results sorted by theme, plus a mocked QtListWidgetItem
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
mock_search_results = []
mock_song = MagicMock()
mock_song.id = 1
mock_song.title = 'My Song'
mock_song.sort_key = 'My Song'
mock_song.theme_name = 'My Theme'
mock_song.temporary = False
mock_search_results.append(mock_song)
mock_qlist_widget = MagicMock()
MockedQListWidgetItem.return_value = mock_qlist_widget
# WHEN: I display song search results sorted by theme
self.media_item.display_results_themes(mock_search_results)
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
self.media_item.list_view.clear.assert_called_with()
MockedQListWidgetItem.assert_called_with('My Theme (My Song)')
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
def display_results_cclinumber_test(self):
"""
Test displaying song search results sorted by CCLI number with basic song
"""
# GIVEN: Search results sorted by CCLI number, plus a mocked QtListWidgetItem
with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \
patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole:
mock_search_results = []
mock_song = MagicMock()
mock_song.id = 1
mock_song.title = 'My Song'
mock_song.sort_key = 'My Song'
mock_song.ccli_number = '12345'
mock_song.temporary = False
mock_search_results.append(mock_song)
mock_qlist_widget = MagicMock()
MockedQListWidgetItem.return_value = mock_qlist_widget
# WHEN: I display song search results sorted by CCLI number
self.media_item.display_results_cclinumber(mock_search_results)
# THEN: The current list view is cleared, the widget is created, and the relevant attributes set
self.media_item.list_view.clear.assert_called_with()
MockedQListWidgetItem.assert_called_with('12345 (My Song)')
mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id)
self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget)
def build_song_footer_one_author_test(self):
"""
Test build songs footer with basic song and one author
@ -265,6 +416,19 @@ class TestMediaItem(TestCase, TestMixin):
# THEN: They should not match
self.assertFalse(result, "Authors should not match")
def natural_sort_key_test(self):
"""
Test the _natural_sort_key function
"""
# GIVEN: A string to be converted into a sort key
string_sort_key = 'A1B12C'
# WHEN: We attempt to create a sort key
sort_key_result = self.media_item._natural_sort_key(string_sort_key)
# THEN: We should get back a tuple split on integers
self.assertEqual(sort_key_result, ['a', 1, 'b', 12, 'c'])
def build_remote_search_test(self):
"""
Test results for the remote search api

View File

@ -81,3 +81,19 @@ class TestSongUsage(TestCase):
# THEN: It should return True
self.assertTrue(ret)
@patch('openlp.plugins.songusage.songusageplugin.Manager')
def toggle_song_usage_state_test(self, MockedManager):
"""
Test that toggle_song_usage_state does toggle song_usage_state
"""
# GIVEN: A SongUsagePlugin
song_usage = SongUsagePlugin()
song_usage.set_button_state = MagicMock()
song_usage.song_usage_active = True
# WHEN: calling toggle_song_usage_state
song_usage.toggle_song_usage_state()
# THEN: song_usage_state should have been toogled
self.assertFalse(song_usage.song_usage_active)

View File

@ -32,7 +32,7 @@ from PyQt5 import QtWidgets
from openlp.core.common import Registry, Settings
from openlp.core.lib.pluginmanager import PluginManager
from tests.interfaces import MagicMock
from tests.interfaces import MagicMock, patch
from tests.helpers.testmixin import TestMixin
@ -45,13 +45,12 @@ class TestPluginManager(TestCase, TestMixin):
"""
Some pre-test setup required.
"""
Settings.setDefaultFormat(Settings.IniFormat)
self.setup_application()
self.build_settings()
self.temp_dir = mkdtemp('openlp')
Settings().setValue('advanced/data path', self.temp_dir)
Registry.create()
Registry().register('service_list', MagicMock())
self.setup_application()
self.main_window = QtWidgets.QMainWindow()
Registry().register('main_window', self.main_window)
@ -64,7 +63,13 @@ class TestPluginManager(TestCase, TestMixin):
gc.collect()
shutil.rmtree(self.temp_dir)
def find_plugins_test(self):
@patch('openlp.plugins.songusage.lib.db.init_schema')
@patch('openlp.plugins.songs.lib.db.init_schema')
@patch('openlp.plugins.images.lib.db.init_schema')
@patch('openlp.plugins.custom.lib.db.init_schema')
@patch('openlp.plugins.alerts.lib.db.init_schema')
@patch('openlp.plugins.bibles.lib.db.init_schema')
def find_plugins_test(self, mocked_is1, mocked_is2, mocked_is3, mocked_is4, mocked_is5, mocked_is6):
"""
Test the find_plugins() method to ensure it imports the correct plugins
"""

View File

@ -128,3 +128,19 @@ class TestEditCustomForm(TestCase, TestMixin):
# THEN: The validate method should have returned False.
assert not result, 'The _validate() method should have retured False'
mocked_critical_error_message_box.assert_called_with(message='You need to add at least one slide.')
def update_slide_list_test(self):
"""
Test the update_slide_list() method
"""
# GIVEN: Mocked slide_list_view with a slide with 3 lines
self.form.slide_list_view = MagicMock()
self.form.slide_list_view.count.return_value = 1
self.form.slide_list_view.currentRow.return_value = 0
self.form.slide_list_view.item.return_value = MagicMock(return_value='1st Slide\n2nd Slide\n3rd Slide')
# WHEN: updating the slide by splitting the lines into slides
self.form.update_slide_list(['1st Slide', '2nd Slide', '3rd Slide'])
# THEN: The slides should be created in correct order
self.form.slide_list_view.addItems.assert_called_with(['1st Slide', '2nd Slide', '3rd Slide'])