merge w/ trunk

This commit is contained in:
Chris Hill 2016-03-06 09:02:38 +00:00
commit 7971daacdc
12 changed files with 347 additions and 12 deletions

View File

@ -515,7 +515,7 @@ class PJLink1(QTcpSocket):
self.socket_timer.start() self.socket_timer.start()
try: try:
self.projectorNetwork.emit(S_NETWORK_SENDING) self.projectorNetwork.emit(S_NETWORK_SENDING)
sent = self.write(out) sent = self.write(out.encode('ascii'))
self.waitForBytesWritten(2000) # 2 seconds should be enough self.waitForBytesWritten(2000) # 2 seconds should be enough
if sent == -1: if sent == -1:
# Network error? # Network error?
@ -665,7 +665,15 @@ class PJLink1(QTcpSocket):
:param data: Class that projector supports. :param data: Class that projector supports.
""" """
self.pjlink_class = data # bug 1550891: Projector returns non-standard class response:
# : Expected: %1CLSS=1
# : Received: %1CLSS=Class 1
if len(data) > 1:
# Split non-standard information from response
clss = data.split()[-1]
else:
clss = data
self.pjlink_class = clss
log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class))
return return

View File

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

View File

@ -601,13 +601,21 @@ class SlideController(DisplayController, RegistryProperties):
def __add_actions_to_widget(self, widget): def __add_actions_to_widget(self, widget):
""" """
Add actions to the widget specified by `widget` Add actions to the widget specified by `widget`
This defines the controls available when Live display has stolen focus.
Examples of this happening: Clicking anything in the live window or certain single screen mode scenarios.
Needles to say, blank to modes should not be removed from here.
For some reason this required a test. It may be found in test_slidecontroller.py as
"live_stolen_focus_shortcuts_test. If you want to modify things here, you must also modify them there. (Duh)
:param widget: The UI widget for the actions :param widget: The UI widget for the actions
""" """
widget.addActions([ widget.addActions([
self.previous_item, self.next_item, self.previous_item, self.next_item,
self.previous_service, self.next_service, self.previous_service, self.next_service,
self.escape_item]) self.escape_item,
self.desktop_screen,
self.theme_screen,
self.blank_screen])
def preview_size_changed(self): def preview_size_changed(self):
""" """

View File

@ -26,7 +26,7 @@ import os
import shutil import shutil
from PyQt5 import QtCore, QtWidgets 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.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate
from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ 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.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm from openlp.plugins.songs.forms.songexportform import SongExportForm
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song 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.ui import SongStrings
from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML
@ -52,8 +52,11 @@ class SongSearch(object):
Titles = 2 Titles = 2
Lyrics = 3 Lyrics = 3
Authors = 4 Authors = 4
Books = 5 Topics = 5
Themes = 6 Books = 6
Themes = 7
Copyright = 8
CCLInumber = 9
class SongMediaItem(MediaManagerItem): class SongMediaItem(MediaManagerItem):
@ -151,9 +154,17 @@ class SongMediaItem(MediaManagerItem):
translate('SongsPlugin.MediaItem', 'Search Lyrics...')), translate('SongsPlugin.MediaItem', 'Search Lyrics...')),
(SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors, (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors,
translate('SongsPlugin.MediaItem', 'Search 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, (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks,
translate('SongsPlugin.MediaItem', 'Search 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.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section))
self.config_update() self.config_update()
@ -184,14 +195,33 @@ class SongMediaItem(MediaManagerItem):
search_results = self.plugin.manager.get_all_objects( search_results = self.plugin.manager.get_all_objects(
Author, Author.display_name.like(search_string), Author.display_name.asc()) Author, Author.display_name.like(search_string), Author.display_name.asc())
self.display_results_author(search_results) 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: elif search_type == SongSearch.Books:
log.debug('Songbook Search') log.debug('Songbook Search')
self.display_results_book(search_keywords) self.display_results_book(search_keywords)
elif search_type == SongSearch.Themes: elif search_type == SongSearch.Themes:
log.debug('Theme Search') log.debug('Theme Search')
search_string = '%' + search_keywords + '%' 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) 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() self.check_search_result()
def search_entire(self, search_keywords): def search_entire(self, search_keywords):
@ -215,6 +245,12 @@ class SongMediaItem(MediaManagerItem):
log.debug('on_song_list_load - finished') log.debug('on_song_list_load - finished')
def display_results_song(self, search_results): 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') log.debug('display results Song')
self.save_auto_select_id() self.save_auto_select_id()
self.list_view.clear() self.list_view.clear()
@ -234,6 +270,12 @@ class SongMediaItem(MediaManagerItem):
self.auto_select_id = -1 self.auto_select_id = -1
def display_results_author(self, search_results): 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') log.debug('display results Author')
self.list_view.clear() self.list_view.clear()
for author in search_results: for author in search_results:
@ -247,6 +289,13 @@ class SongMediaItem(MediaManagerItem):
self.list_view.addItem(song_name) self.list_view.addItem(song_name)
def display_results_book(self, search_keywords): 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') log.debug('display results Book')
self.list_view.clear() self.list_view.clear()
@ -269,6 +318,64 @@ class SongMediaItem(MediaManagerItem):
song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id) song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id)
self.list_view.addItem(song_name) 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): def on_clear_text_button_click(self):
""" """
Clear the search text. Clear the search text.

View File

@ -3,8 +3,11 @@
<file>song_search_stop.png</file> <file>song_search_stop.png</file>
<file>song_search_all.png</file> <file>song_search_all.png</file>
<file>song_search_author.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_lyrics.png</file>
<file>song_search_title.png</file> <file>song_search_title.png</file>
<file>song_search_topic.png</file>
<file>topic_edit.png</file> <file>topic_edit.png</file>
<file>author_add.png</file> <file>author_add.png</file>
<file>author_delete.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

@ -60,3 +60,17 @@ class TestPJLink(TestCase):
"Connection request should have been called with TEST_SALT")) "Connection request should have been called with TEST_SALT"))
self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN, self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN,
"Connection request should have been called with TEST_PIN")) "Connection request should have been called with TEST_PIN"))
def non_standard_class_reply_test(self):
"""
bugfix 1550891 - CLSS request returns non-standard 'Class N' reply
"""
# GIVEN: Test object
pjlink = pjlink_test
# WHEN: Process non-standard reply
pjlink.process_clss('Class 1')
# THEN: Projector class should be set with proper value
self.assertEquals(pjlink.pjlink_class, '1',
'Non-standard class reply should have set proper class')

View File

@ -685,6 +685,34 @@ class TestSlideController(TestCase):
self.assertEqual('mocked_presentation_item_stop', mocked_execute.call_args_list[1][0][0], self.assertEqual('mocked_presentation_item_stop', mocked_execute.call_args_list[1][0][0],
'The presentation should have been stopped.') 'The presentation should have been stopped.')
def live_stolen_focus_shortcuts_test(self):
"""
Test that all the needed shortcuts are available in scenarios where Live has stolen focus.
These are found under def __add_actions_to_widget(self, widget): in slidecontroller.py
"""
# GIVEN: A slide controller, actions needed
slide_controller = SlideController(None)
mocked_widget = MagicMock()
slide_controller.previous_item = MagicMock()
slide_controller.next_item = MagicMock()
slide_controller.previous_service = MagicMock()
slide_controller.next_service = MagicMock()
slide_controller.escape_item = MagicMock()
slide_controller.desktop_screen = MagicMock()
slide_controller.blank_screen = MagicMock()
slide_controller.theme_screen = MagicMock()
# WHEN: __add_actions_to_widget is called
slide_controller._SlideController__add_actions_to_widget(mocked_widget)
# THEN: The call to addActions should be correct
mocked_widget.addActions.assert_called_with([
slide_controller.previous_item, slide_controller.next_item,
slide_controller.previous_service, slide_controller.next_service,
slide_controller.escape_item, slide_controller.desktop_screen,
slide_controller.theme_screen, slide_controller.blank_screen
])
class TestInfoLabel(TestCase): class TestInfoLabel(TestCase):

View File

@ -48,6 +48,12 @@ class TestMediaItem(TestCase, TestMixin):
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \ with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \
patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'):
self.media_item = SongMediaItem(None, MagicMock()) 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_songbook = False
self.media_item.display_copyright_symbol = False self.media_item.display_copyright_symbol = False
self.setup_application() self.setup_application()
@ -60,6 +66,151 @@ class TestMediaItem(TestCase, TestMixin):
""" """
self.destroy_settings() 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): def build_song_footer_one_author_test(self):
""" """
Test build songs footer with basic song and one author Test build songs footer with basic song and one author

View File

@ -81,3 +81,19 @@ class TestSongUsage(TestCase):
# THEN: It should return True # THEN: It should return True
self.assertTrue(ret) 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)