From c8f200b20fd77b5b4486a1306f1bdd96e2cd9ea4 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Fri, 13 Feb 2015 18:29:42 +0000 Subject: [PATCH 01/23] fixed bug #1280295 'Enable natural sorting for song book searches' --fixes 1280294 --- openlp/plugins/songs/lib/mediaitem.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index c0c58ff90..ae804878e 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -255,7 +255,7 @@ class SongMediaItem(MediaManagerItem): log.debug('display results Book') self.list_view.clear() for book in search_results: - songs = sorted(book.songs, key=lambda song: int(re.match(r'[0-9]+', '0' + song.song_number).group())) + songs = sorted(book.songs, key=lambda song: self._natural_sort_key(song.song_number)) for song in songs: # Do not display temporary songs if song.temporary: @@ -583,6 +583,24 @@ class SongMediaItem(MediaManagerItem): # List must be empty at the end return not author_list + def _try_int(self, s): + """ + Convert string s to an integer if possible. Fail silently and return + the string as-is if it isn't an integer. + :param s: The string to try to convert. + """ + try: + return int(s) + except (TypeError, ValueError): + return s + + 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 list(map(self._try_int, re.findall(r'(\d+|\D+)', s))) + def search(self, string, show_error): """ Search for some songs From 81908970b341c10ba39d057f16ba3259040befdd Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 20 Jun 2015 23:29:03 +0100 Subject: [PATCH 02/23] added unit tests --- .../openlp_plugins/songs/test_mediaitem.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 4e28eed93..e3585546f 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,6 +48,10 @@ 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.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.display_songbook = False self.media_item.display_copyright_symbol = False self.setup_application() @@ -60,6 +64,37 @@ class TestMediaItem(TestCase, TestMixin): """ self.destroy_settings() + def display_results_book_test(self): + """ + Test displaying song search results grouped by book with basic song + """ + # GIVEN: Search results grouped by book, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_book = MagicMock() + mock_song = MagicMock() + mock_book.name = 'My Book' + mock_book.songs = [] + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.song_number = '123' + mock_song.temporary = False + mock_book.songs.append(mock_song) + mock_search_results.append(mock_book) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by book + self.media_item.display_results_book(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 Book - 123 (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 @@ -257,3 +292,44 @@ class TestMediaItem(TestCase, TestMixin): # THEN: They should not match self.assertFalse(result, "Authors should not match") + + def try_int_with_string_integer_test(self): + """ + Test the _try_int function with a string containing an integer + """ + # GIVEN: A string that is an integer + string_integer = '123' + + # WHEN: We "convert" it to an integer + integer_result = self.media_item._try_int(string_integer) + + # THEN: We should get back an integer + self.assertIsInstance(integer_result, int, 'The result should be an integer') + self.assertEqual(integer_result, 123, 'The result should be 123') + + def try_int_with_string_noninteger_test(self): + """ + Test the _try_int function with a string not containing an integer + """ + # GIVEN: A string that is not an integer + string_noninteger = 'abc' + + # WHEN: We "convert" it to an integer + noninteger_result = self.media_item._try_int(string_noninteger) + + # THEN: We should get back the original string + self.assertIsInstance(noninteger_result, type(string_noninteger), 'The result type should be the same') + self.assertEqual(noninteger_result, string_noninteger, 'The result value should be the same') + + 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 = 'A1B12C123' + + # 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', 123]) From 442f9578d1658314d6253ccea510f3df4f6f9171 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 13 Feb 2016 17:09:46 +0000 Subject: [PATCH 03/23] test fix for trunk --- .../openlp_plugins/songs/test_mediaitem.py | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 0f7cdb7fc..428f9bca3 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,10 +48,6 @@ 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.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.display_songbook = False self.media_item.display_copyright_symbol = False self.setup_application() @@ -64,37 +60,6 @@ class TestMediaItem(TestCase, TestMixin): """ self.destroy_settings() - def display_results_book_test(self): - """ - Test displaying song search results grouped by book with basic song - """ - # GIVEN: Search results grouped by book, plus a mocked QtListWidgetItem - with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ - patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: - mock_search_results = [] - mock_book = MagicMock() - mock_song = MagicMock() - mock_book.name = 'My Book' - mock_book.songs = [] - mock_song.id = 1 - mock_song.title = 'My Song' - mock_song.sort_key = 'My Song' - mock_song.song_number = '123' - mock_song.temporary = False - mock_book.songs.append(mock_song) - mock_search_results.append(mock_book) - mock_qlist_widget = MagicMock() - MockedQListWidgetItem.return_value = mock_qlist_widget - - # WHEN: I display song search results grouped by book - self.media_item.display_results_book(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 Book - 123 (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 From 7887dcbf2b977fb851ab725d3ae849c9beca41d9 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 13 Feb 2016 17:11:35 +0000 Subject: [PATCH 04/23] cosmetic --- openlp/plugins/songs/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index fda607bbe..420379e2b 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -253,7 +253,7 @@ class SongMediaItem(MediaManagerItem): search_keywords = search_keywords.rpartition(' ') search_book = search_keywords[0] search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) - + songbook_entries = (self.plugin.manager.session.query(SongBookEntry) .join(Book)) songbook_entries = sorted(songbook_entries, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) From 1e94cd92e9e86f409fca53f8d6cdae7f89847d25 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 29 Feb 2016 15:31:05 +1030 Subject: [PATCH 05/23] Split auto-scroll & height cap features to new branch --- openlp/core/ui/listpreviewwidget.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index fb6481e56..496c3e8ec 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -47,6 +47,9 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ super(QtWidgets.QTableWidget, self).__init__(parent) self._setup(screen_ratio) + + # max row height for non-text slides in pixels. If <= 0, will disable max row height. + self.max_img_row_height = 200 def _setup(self, screen_ratio): """ @@ -82,8 +85,10 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): self.resizeRowsToContents() else: # Sort out image heights. + height = self.viewport().width() // self.screen_ratio ### Moved out of loop as only needs to run once + if self.max_img_row_height > 0 and height > self.max_img_row_height: ### Apply row height cap. + height = self.max_img_row_height for frame_number in range(len(self.service_item.get_frames())): - height = self.viewport().width() // self.screen_ratio self.setRowHeight(frame_number, height) def screen_size_changed(self, screen_ratio): @@ -139,7 +144,20 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap = QtGui.QPixmap.fromImage(image) pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) - self.setCellWidget(frame_number, 0, label) + ### begin added/modified content + if self.max_img_row_height > 0: + label.setMaximumWidth(self.max_img_row_height * self.screen_ratio) ### set max width based on max height + label.resize(self.max_img_row_height * self.screen_ratio,self.max_img_row_height) ### resize to max width and max height; may be adjusted when setRowHeight called. + container = QtWidgets.QWidget() ### container widget + hbox = QtWidgets.QHBoxLayout() ### hbox to allow for horizonal stretch padding + hbox.setContentsMargins(0, 0, 0, 0) ### 0 contents margins to avoid extra padding + hbox.addWidget(label,stretch=1) ### add slide, stretch allows growing to max-width + hbox.addStretch(0) ### add strech padding with lowest priority; will only grow when slide has hit max-width + container.setLayout(hbox) ### populate container widget + self.setCellWidget(frame_number, 0, container) ### populate cell with container + else: + self.setCellWidget(frame_number, 0, label) ### populate cell with slide + ### end added/modified content slide_height = width // self.screen_ratio row += 1 text.append(str(row)) From 68460f5e3f5a8813481840d42a5ef727c35854c9 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Sun, 6 Mar 2016 03:11:32 +1030 Subject: [PATCH 06/23] Added smart scaling when manually resized, integrated with settings dialog, fixed some pep8 errors --- openlp/core/common/settings.py | 1 + openlp/core/ui/advancedtab.py | 12 ++++ openlp/core/ui/listpreviewwidget.py | 64 ++++++++++++------- .../openlp_core_ui/test_listpreviewwidget.py | 4 +- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 8ef2b3c8b..5c103ed9a 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -121,6 +121,7 @@ class Settings(QtCore.QSettings): 'advanced/double click live': False, 'advanced/enable exit confirmation': True, 'advanced/expand service item': False, + 'advanced/slide max height': 0, 'advanced/hide mouse': True, 'advanced/is portable': False, 'advanced/max recent files': 20, diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 4421b432f..bc9fe520f 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -80,6 +80,13 @@ class AdvancedTab(SettingsTab): 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) + self.slide_max_height_label = QtWidgets.QLabel(self.ui_group_box) + self.slide_max_height_label.setObjectName('slide_max_height_label') + self.slide_max_height_spin_box = QtWidgets.QSpinBox(self.ui_group_box) + self.slide_max_height_spin_box.setObjectName('slide_max_height_spin_box') + self.slide_max_height_spin_box.setRange(0,1000) + self.slide_max_height_spin_box.setSingleStep(20) + self.ui_layout.addRow(self.slide_max_height_label,self.slide_max_height_spin_box) self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.search_as_type_check_box.setObjectName('SearchAsType_check_box') self.ui_layout.addRow(self.search_as_type_check_box) @@ -272,6 +279,9 @@ class AdvancedTab(SettingsTab): 'Preview items when clicked in Media 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', + 'Max height for non-text slides\nin slide controller:')) + self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) @@ -340,6 +350,7 @@ class AdvancedTab(SettingsTab): 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.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')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) self.service_name_day.setCurrentIndex(settings.value('default service day')) @@ -421,6 +432,7 @@ class AdvancedTab(SettingsTab): 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('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()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 496c3e8ec..68c983d42 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -26,7 +26,7 @@ It is based on a QTableWidget but represents its contents in list form. from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import RegistryProperties +from openlp.core.common import RegistryProperties, Settings from openlp.core.lib import ImageSource, ServiceItem @@ -47,9 +47,6 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ super(QtWidgets.QTableWidget, self).__init__(parent) self._setup(screen_ratio) - - # max row height for non-text slides in pixels. If <= 0, will disable max row height. - self.max_img_row_height = 200 def _setup(self, screen_ratio): """ @@ -66,6 +63,8 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): # Initialize variables. self.service_item = ServiceItem() self.screen_ratio = screen_ratio + # Connect signals + self.verticalHeader().sectionResized.connect(self.row_resized) def resizeEvent(self, event): """ @@ -83,14 +82,30 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): # Sort out songs, bibles, etc. if self.service_item.is_text(): self.resizeRowsToContents() + # Sort out image heights. else: - # Sort out image heights. - height = self.viewport().width() // self.screen_ratio ### Moved out of loop as only needs to run once - if self.max_img_row_height > 0 and height > self.max_img_row_height: ### Apply row height cap. - height = self.max_img_row_height + height = self.viewport().width() // self.screen_ratio + max_img_row_height = Settings().value('advanced/slide max height') + # Adjust for row height cap if in use. + if max_img_row_height > 0 and height > max_img_row_height: + height = max_img_row_height + # Apply new height to slides for frame_number in range(len(self.service_item.get_frames())): self.setRowHeight(frame_number, height) + def row_resized(self, row, old_height, new_height): + """ + Will scale non-image slides. + """ + # Only for non-text slides when row height cap in use + if self.service_item.is_text() or Settings().value('advanced/slide max height') <= 0: + return + # Get and validate label widget containing slide & adjust max width + try: + self.cellWidget(row, 0).children()[1].setMaximumWidth(new_height * self.screen_ratio) + except: + return + def screen_size_changed(self, screen_ratio): """ This method is called whenever the live screen size changes, which then makes a layout recalculation necessary @@ -144,21 +159,26 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap = QtGui.QPixmap.fromImage(image) pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) - ### begin added/modified content - if self.max_img_row_height > 0: - label.setMaximumWidth(self.max_img_row_height * self.screen_ratio) ### set max width based on max height - label.resize(self.max_img_row_height * self.screen_ratio,self.max_img_row_height) ### resize to max width and max height; may be adjusted when setRowHeight called. - container = QtWidgets.QWidget() ### container widget - hbox = QtWidgets.QHBoxLayout() ### hbox to allow for horizonal stretch padding - hbox.setContentsMargins(0, 0, 0, 0) ### 0 contents margins to avoid extra padding - hbox.addWidget(label,stretch=1) ### add slide, stretch allows growing to max-width - hbox.addStretch(0) ### add strech padding with lowest priority; will only grow when slide has hit max-width - container.setLayout(hbox) ### populate container widget - self.setCellWidget(frame_number, 0, container) ### populate cell with container - else: - self.setCellWidget(frame_number, 0, label) ### populate cell with slide - ### end added/modified content slide_height = width // self.screen_ratio + # Setup row height cap if in use. + max_img_row_height = Settings().value('advanced/slide max height') + if max_img_row_height > 0: + if slide_height > max_img_row_height: + slide_height = max_img_row_height + label.setMaximumWidth(max_img_row_height * self.screen_ratio) + label.resize(max_img_row_height * self.screen_ratio, max_img_row_height) + # Build widget with stretch padding + container = QtWidgets.QWidget() + hbox = QtWidgets.QHBoxLayout() + hbox.setContentsMargins(0, 0, 0, 0) + hbox.addWidget(label, stretch=1) + hbox.addStretch(0) + container.setLayout(hbox) + # Add to table + self.setCellWidget(frame_number, 0, container) + else: + # Add to table + self.setCellWidget(frame_number, 0, label) row += 1 text.append(str(row)) self.setItem(frame_number, 0, item) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index 6f27fbde3..5d1135e23 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -23,9 +23,11 @@ Package to test the openlp.core.ui.listpreviewwidget package. """ from unittest import TestCase + +from openlp.core.common import Settings from openlp.core.ui.listpreviewwidget import ListPreviewWidget -from tests.functional import patch +from tests.functional import MagicMock, patch class TestListPreviewWidget(TestCase): From 79b4c474d613c101ba125b5413002e2e299b8619 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Sat, 19 Mar 2016 19:10:11 +1030 Subject: [PATCH 07/23] Added testing --- .../openlp_core_ui/test_listpreviewwidget.py | 263 +++++++++++++++++- 1 file changed, 258 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index 5d1135e23..e4cd334d4 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -26,19 +26,23 @@ from unittest import TestCase from openlp.core.common import Settings from openlp.core.ui.listpreviewwidget import ListPreviewWidget +from openlp.core.lib import ServiceItem -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, patch, call class TestListPreviewWidget(TestCase): + def setUp(self): """ Mock out stuff for all the tests """ - self.setup_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget._setup') - self.mocked_setup = self.setup_patcher.start() - self.addCleanup(self.setup_patcher.stop) + self.parent_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.parent') + self.mocked_parent = self.parent_patcher.start() + self.mocked_parent.width.return_value = 100 + self.addCleanup(self.parent_patcher.stop) + def new_list_preview_widget_test(self): """ @@ -51,4 +55,253 @@ class TestListPreviewWidget(TestCase): # THEN: The object is not None, and the _setup() method was called. self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') - self.mocked_setup.assert_called_with(1) + self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') + #self.mocked_setup.assert_called_with(1) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in replace_service_item and __recalculate_layout. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # a text ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock text service item + service_item = MagicMock() + service_item.is_text.return_value = True + service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None}, + {'title': None, 'text': None, 'verseTag': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: resizeRowsToContents should be called twice + # (once each in __recalculate_layout and replace_service_item) + self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') + self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" disabled, image-based slides not resized to the max-height in replace_service_item and __recalculate_layout. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 0 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: timer should have been started + self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') + self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') + calls = [call(0,200), call(1,200),call(0,400), call(1,400)] + mocked_setRowHeight.assert_has_calls(calls) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, image-based slides are resized to the max-height in replace_service_item and __recalculate_layout. + """ + + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: timer should have been started + self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') + self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') + calls = [call(0,100), call(1,100),call(0,100), call(1,100)] + mocked_setRowHeight.assert_has_calls(calls) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # a text ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock text service item + service_item = MagicMock() + service_item.is_text.return_value = True + service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None}, + {'title': None, 'text': None, 'verseTag': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called + self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" disabled, image-based slides not affected in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 0 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called + self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, image-based slides are scaled in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called + mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) From bb0adc6f5dc1afdb3a15e69ea8682f8f63675423 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 15:01:10 +0000 Subject: [PATCH 08/23] fixed bug #1280295 'Enable natural sorting for song book searches', refactored to move filtering to database, updated test Fixes: https://launchpad.net/bugs/1280295 --- openlp/plugins/songs/lib/mediaitem.py | 30 +++++++---------- .../openlp_plugins/songs/test_mediaitem.py | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 12579c56d..12ef3de3d 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -203,7 +203,13 @@ class SongMediaItem(MediaManagerItem): self.display_results_topic(search_results) elif search_type == SongSearch.Books: log.debug('Songbook Search') - self.display_results_book(search_keywords) + search_keywords = search_keywords.rpartition(' ') + search_book = search_keywords[0] + '%' + search_entry = search_keywords[2] + '%' + search_results = (self.plugin.manager.session.query(SongBookEntry) + .join(Book) + .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) + self.display_results_book(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') search_string = '%' + search_keywords + '%' @@ -288,31 +294,19 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) - def display_results_book(self, search_keywords): + def display_results_book(self, search_results): """ - Display the song search results in the media manager list, grouped by book + Display the song search results in the media manager list, grouped by book and entry - :param search_keywords: A list of search keywords - book first, then number + :param search_results: A list of db SongBookEntry objects :return: None """ - log.debug('display results Book') self.list_view.clear() - - search_keywords = search_keywords.rpartition(' ') - search_book = search_keywords[0] - search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) - - songbook_entries = (self.plugin.manager.session.query(SongBookEntry) - .join(Book)) - songbook_entries = sorted(songbook_entries, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) - for songbook_entry in songbook_entries: + search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + for songbook_entry in search_results: if songbook_entry.song.temporary: continue - if search_book.lower() not in songbook_entry.songbook.name.lower(): - continue - if search_entry not in songbook_entry.entry: - continue song_detail = '%s #%s: %s' % (songbook_entry.songbook.name, songbook_entry.entry, songbook_entry.song.title) song_name = QtWidgets.QListWidgetItem(song_detail) song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index d09f5b76e..865b81ba9 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -127,6 +127,38 @@ class TestMediaItem(TestCase, TestMixin): 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_book_test(self): + """ + Test displaying song search results grouped by book and entry with basic song + """ + # GIVEN: Search results grouped by book and entry, 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_songbook_entry = MagicMock() + mock_songbook = MagicMock() + mock_song = MagicMock() + mock_songbook_entry.entry = '1' + mock_songbook.name = 'My Book' + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.temporary = False + mock_songbook_entry.song = mock_song + mock_songbook_entry.songbook = mock_songbook + mock_search_results.append(mock_songbook_entry) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by book + self.media_item.display_results_book(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 Book #1: My Song') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_songbook_entry.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 From 3db138ea8d1334a2aabec934c3b663260143eea8 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 15:50:56 +0000 Subject: [PATCH 09/23] coding standards fix --- openlp/plugins/songs/lib/mediaitem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 67dee4023..06636bcc6 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -207,8 +207,8 @@ class SongMediaItem(MediaManagerItem): search_book = search_keywords[0] + '%' search_entry = search_keywords[2] + '%' search_results = (self.plugin.manager.session.query(SongBookEntry) - .join(Book) - .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) + .join(Book) + .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) self.display_results_book(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') @@ -303,7 +303,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: ( + songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From 5e33c0508055807318d7ca9c559b718025496a09 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 16:00:35 +0000 Subject: [PATCH 10/23] coding standards fix 2 --- openlp/plugins/songs/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 06636bcc6..fd7dfe986 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -304,7 +304,7 @@ class SongMediaItem(MediaManagerItem): log.debug('display results Book') self.list_view.clear() search_results = sorted(search_results, key=lambda songbook_entry: ( - songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From 2feedf6f383c8fad2e11b4928900a43bf51ee668 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 16:01:23 +0000 Subject: [PATCH 11/23] coding standards fix --- openlp/plugins/songs/lib/mediaitem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index fd7dfe986..e46b95fbf 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -303,8 +303,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: ( - songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, + self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From 4f1a07454696266d70f74e5ebb5187ac7ca42fc1 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 21 Mar 2016 01:04:52 +1030 Subject: [PATCH 12/23] Cleaned pep8 errors --- .../openlp_core_ui/test_listpreviewwidget.py | 73 ++++++++----------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index e4cd334d4..df0092452 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -33,7 +33,6 @@ from tests.functional import MagicMock, patch, call class TestListPreviewWidget(TestCase): - def setUp(self): """ Mock out stuff for all the tests @@ -42,7 +41,6 @@ class TestListPreviewWidget(TestCase): self.mocked_parent = self.parent_patcher.start() self.mocked_parent.width.return_value = 100 self.addCleanup(self.parent_patcher.stop) - def new_list_preview_widget_test(self): """ @@ -56,21 +54,19 @@ class TestListPreviewWidget(TestCase): # THEN: The object is not None, and the _setup() method was called. self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') - #self.mocked_setup.assert_called_with(1) - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in replace_service_item and __recalculate_layout. + Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc... """ # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # a text ServiceItem and a ListPreviewWidget. - + # Mock Settings().value('advanced/slide max height') mocked_Settings_obj = MagicMock() mocked_Settings_obj.value.return_value = 100 @@ -89,28 +85,27 @@ class TestListPreviewWidget(TestCase): list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize mocked_viewport_obj.width.return_value = 400 - + # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - + # THEN: resizeRowsToContents should be called twice # (once each in __recalculate_layout and replace_service_item) self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" disabled, image-based slides not resized to the max-height in replace_service_item and __recalculate_layout. + Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc... """ # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # an image ServiceItem and a ListPreviewWidget. - + # Mock Settings().value('advanced/slide max height') mocked_Settings_obj = MagicMock() mocked_Settings_obj.value.return_value = 0 @@ -129,27 +124,26 @@ class TestListPreviewWidget(TestCase): list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize mocked_viewport_obj.width.return_value = 400 - + # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - + # THEN: timer should have been started self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') - calls = [call(0,200), call(1,200),call(0,400), call(1,400)] + calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" enabled, image-based slides are resized to the max-height in replace_service_item and __recalculate_layout. + Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc... """ - + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # an image ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') @@ -170,30 +164,29 @@ class TestListPreviewWidget(TestCase): list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize mocked_viewport_obj.width.return_value = 400 - + # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - + # THEN: timer should have been started self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') - calls = [call(0,100), call(1,100),call(0,100), call(1,100)] + calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in row_resized. + Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized. """ # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # a text ServiceItem and a ListPreviewWidget. - + # Mock Settings().value('advanced/slide max height') mocked_Settings_obj = MagicMock() mocked_Settings_obj.value.return_value = 100 @@ -210,7 +203,7 @@ class TestListPreviewWidget(TestCase): # Mock self.cellWidget().children().setMaximumWidth() mocked_cellWidget_child = MagicMock() mocked_cellWidget_obj = MagicMock() - mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child] mocked_cellWidget.return_value = mocked_cellWidget_obj # init ListPreviewWidget and load service item list_preview_widget = ListPreviewWidget(None, 1) @@ -218,24 +211,23 @@ class TestListPreviewWidget(TestCase): # WHEN: row_resized() is called list_preview_widget.row_resized(0, 100, 150) - + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" disabled, image-based slides not affected in row_resized. + Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized. """ # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # an image ServiceItem and a ListPreviewWidget. - + # Mock Settings().value('advanced/slide max height') mocked_Settings_obj = MagicMock() mocked_Settings_obj.value.return_value = 0 @@ -252,7 +244,7 @@ class TestListPreviewWidget(TestCase): # Mock self.cellWidget().children().setMaximumWidth() mocked_cellWidget_child = MagicMock() mocked_cellWidget_obj = MagicMock() - mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child] mocked_cellWidget.return_value = mocked_cellWidget_obj # init ListPreviewWidget and load service item list_preview_widget = ListPreviewWidget(None, 1) @@ -260,24 +252,23 @@ class TestListPreviewWidget(TestCase): # WHEN: row_resized() is called list_preview_widget.row_resized(0, 100, 150) - + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + mocked_viewport, mocked_Settings): """ - Test if "Max height for non-text slides in slide controller" enabled, image-based slides are scaled in row_resized. + Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized. """ # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # an image ServiceItem and a ListPreviewWidget. - + # Mock Settings().value('advanced/slide max height') mocked_Settings_obj = MagicMock() mocked_Settings_obj.value.return_value = 100 @@ -294,7 +285,7 @@ class TestListPreviewWidget(TestCase): # Mock self.cellWidget().children().setMaximumWidth() mocked_cellWidget_child = MagicMock() mocked_cellWidget_obj = MagicMock() - mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget_obj.children.return_value = [None, mocked_cellWidget_child] mocked_cellWidget.return_value = mocked_cellWidget_obj # init ListPreviewWidget and load service item list_preview_widget = ListPreviewWidget(None, 1) @@ -302,6 +293,6 @@ class TestListPreviewWidget(TestCase): # WHEN: row_resized() is called list_preview_widget.row_resized(0, 100, 150) - + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) From 4980465f983cfe973c71df084f50a0650071746b Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 21 Mar 2016 01:14:00 +1030 Subject: [PATCH 13/23] Cleaned pep8 errors --- openlp/core/ui/advancedtab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index bc9fe520f..6b9f9e3cf 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -84,9 +84,9 @@ class AdvancedTab(SettingsTab): self.slide_max_height_label.setObjectName('slide_max_height_label') self.slide_max_height_spin_box = QtWidgets.QSpinBox(self.ui_group_box) self.slide_max_height_spin_box.setObjectName('slide_max_height_spin_box') - self.slide_max_height_spin_box.setRange(0,1000) + self.slide_max_height_spin_box.setRange(0, 1000) self.slide_max_height_spin_box.setSingleStep(20) - self.ui_layout.addRow(self.slide_max_height_label,self.slide_max_height_spin_box) + self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_spin_box) self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.search_as_type_check_box.setObjectName('SearchAsType_check_box') self.ui_layout.addRow(self.search_as_type_check_box) @@ -280,7 +280,7 @@ class AdvancedTab(SettingsTab): 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', - 'Max height for non-text slides\nin slide controller:')) + 'Max height for non-text slides\nin slide controller:')) self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) From a98e62ed5dd787b798b9996a3f9be021405c7af1 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 21 Mar 2016 16:11:46 +1030 Subject: [PATCH 14/23] Corrected comments --- tests/functional/openlp_core_ui/test_listpreviewwidget.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index df0092452..6cbf565b2 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -89,7 +89,7 @@ class TestListPreviewWidget(TestCase): # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - # THEN: resizeRowsToContents should be called twice + # THEN: setRowHeight() should not be called, while resizeRowsToContents() should be called twice # (once each in __recalculate_layout and replace_service_item) self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') @@ -128,7 +128,8 @@ class TestListPreviewWidget(TestCase): # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - # THEN: timer should have been started + # THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called + # twice for each slide. self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)] @@ -168,7 +169,8 @@ class TestListPreviewWidget(TestCase): # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) - # THEN: timer should have been started + # THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called + # twice for each slide. self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] From 1667363abdaf84dc1664306053c117c058441f6c Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Thu, 24 Mar 2016 00:44:37 +1030 Subject: [PATCH 15/23] Shifted common test code into setup. --- .../openlp_core_ui/test_listpreviewwidget.py | 103 +++++++----------- 1 file changed, 39 insertions(+), 64 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index 6cbf565b2..a222189e6 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -37,11 +37,28 @@ class TestListPreviewWidget(TestCase): """ Mock out stuff for all the tests """ + # Mock self.parent().width() self.parent_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.parent') self.mocked_parent = self.parent_patcher.start() self.mocked_parent.width.return_value = 100 self.addCleanup(self.parent_patcher.stop) + # Mock Settings().value() + self.Settings_patcher = patch('openlp.core.ui.listpreviewwidget.Settings') + self.mocked_Settings = self.Settings_patcher.start() + self.mocked_Settings_obj = MagicMock() + self.mocked_Settings_obj.value.return_value = None + self.mocked_Settings.return_value = self.mocked_Settings_obj + self.addCleanup(self.Settings_patcher.stop) + + # Mock self.viewport().width() + self.viewport_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + self.mocked_viewport = self.viewport_patcher.start() + self.mocked_viewport_obj = MagicMock() + self.mocked_viewport_obj.width.return_value = 200 + self.mocked_viewport.return_value = self.mocked_viewport_obj + self.addCleanup(self.viewport_patcher.stop) + def new_list_preview_widget_test(self): """ Test that creating an instance of ListPreviewWidget works @@ -55,12 +72,9 @@ class TestListPreviewWidget(TestCase): self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, txt slides unchanged in replace_service_item & __recalc... """ @@ -68,13 +82,9 @@ class TestListPreviewWidget(TestCase): # a text ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 100 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 100 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock text service item service_item = MagicMock() service_item.is_text.return_value = True @@ -84,7 +94,7 @@ class TestListPreviewWidget(TestCase): list_preview_widget = ListPreviewWidget(None, 1) list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize - mocked_viewport_obj.width.return_value = 400 + self.mocked_viewport_obj.width.return_value = 400 # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) @@ -94,12 +104,9 @@ class TestListPreviewWidget(TestCase): self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, img slides unchanged in replace_service_item & __recalc... """ @@ -107,13 +114,9 @@ class TestListPreviewWidget(TestCase): # an image ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 0 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 0 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock image service item service_item = MagicMock() service_item.is_text.return_value = False @@ -123,7 +126,7 @@ class TestListPreviewWidget(TestCase): list_preview_widget = ListPreviewWidget(None, 1) list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize - mocked_viewport_obj.width.return_value = 400 + self.mocked_viewport_obj.width.return_value = 400 # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) @@ -135,26 +138,19 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') - def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, img slides resized in replace_service_item & __recalc... """ - # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", # an image ServiceItem and a ListPreviewWidget. + # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 100 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 100 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock image service item service_item = MagicMock() service_item.is_text.return_value = False @@ -164,7 +160,7 @@ class TestListPreviewWidget(TestCase): list_preview_widget = ListPreviewWidget(None, 1) list_preview_widget.replace_service_item(service_item, 200, 0) # Change viewport width before forcing a resize - mocked_viewport_obj.width.return_value = 400 + self.mocked_viewport_obj.width.return_value = 400 # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) @@ -176,13 +172,10 @@ class TestListPreviewWidget(TestCase): calls = [call(0, 100), call(1, 100), call(0, 100), call(1, 100)] mocked_setRowHeight.assert_has_calls(calls) - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') - def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, text-based slides not affected in row_resized. """ @@ -190,13 +183,9 @@ class TestListPreviewWidget(TestCase): # a text ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 100 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 100 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock text service item service_item = MagicMock() service_item.is_text.return_value = True @@ -217,13 +206,10 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') - def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." disabled, image-based slides not affected in row_resized. """ @@ -231,13 +217,9 @@ class TestListPreviewWidget(TestCase): # an image ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 0 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 0 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock image service item service_item = MagicMock() service_item.is_text.return_value = False @@ -258,13 +240,10 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') - @patch(u'openlp.core.ui.listpreviewwidget.Settings') - @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') - def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, - mocked_viewport, mocked_Settings): + def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents): """ Test if "Max height for non-text slides..." enabled, image-based slides are scaled in row_resized. """ @@ -272,13 +251,9 @@ class TestListPreviewWidget(TestCase): # an image ServiceItem and a ListPreviewWidget. # Mock Settings().value('advanced/slide max height') - mocked_Settings_obj = MagicMock() - mocked_Settings_obj.value.return_value = 100 - mocked_Settings.return_value = mocked_Settings_obj + self.mocked_Settings_obj.value.return_value = 100 # Mock self.viewport().width() - mocked_viewport_obj = MagicMock() - mocked_viewport_obj.width.return_value = 200 - mocked_viewport.return_value = mocked_viewport_obj + self.mocked_viewport_obj.width.return_value = 200 # Mock image service item service_item = MagicMock() service_item.is_text.return_value = False From 573bc510ff3d270017bd02eeaf7e45138e875658 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Thu, 31 Mar 2016 03:41:52 +1030 Subject: [PATCH 16/23] Added ProPresenter 5 & 6 support --- openlp/plugins/songs/lib/importer.py | 4 +- .../songs/lib/importers/propresenter.py | 96 +++- .../songs/test_propresenterimport.py | 18 +- .../propresentersongs/Amazing Grace.pro5 | 520 ++++++++++++++++++ .../propresentersongs/Amazing Grace.pro6 | 490 +++++++++++++++++ 5 files changed, 1123 insertions(+), 5 deletions(-) create mode 100644 tests/resources/propresentersongs/Amazing Grace.pro5 create mode 100644 tests/resources/propresentersongs/Amazing Grace.pro6 diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index a09bf5ea6..8a79ea04f 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -313,9 +313,9 @@ class SongFormat(object): }, ProPresenter: { 'class': ProPresenterImport, - 'name': 'ProPresenter 4', + 'name': 'ProPresenter 4 -> 6', 'prefix': 'proPresenter', - 'filter': '%s (*.pro4)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter 4 Song Files') + 'filter': '%s (*.pro4 *.pro5 *.pro6)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter Song Files') }, SongBeamer: { 'class': SongBeamerImport, diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index fb47eb1cd..b46f71a08 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -35,7 +35,7 @@ from .songimport import SongImport log = logging.getLogger(__name__) - +''' class ProPresenterImport(SongImport): """ The :class:`ProPresenterImport` class provides OpenLP with the @@ -72,3 +72,97 @@ class ProPresenterImport(SongImport): self.add_verse(words, "v%d" % count) if not self.finish(): self.log_error(self.import_source) +''' + +class ProPresenterImport(SongImport): + """ + The :class:`ProPresenterImport` class provides OpenLP with the + ability to import ProPresenter *4-6* song files. + """ + def do_import(self): + self.import_wizard.progress_bar.setMaximum(len(self.import_source)) + for file_path in self.import_source: + if self.stop_import_flag: + return + self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path)) + root = objectify.parse(open(file_path, 'rb')).getroot() + self.process_song(root, file_path) + + def process_song(self, root, filename): + self.set_defaults() + self.comments = root.get('notes') + self.title = os.path.basename(filename) + try: + self.version = int(root.get('versionNumber')) + except ValueError: + self.log_error(self.import_source) + return + + for author_key in ['author', 'CCLIAuthor', 'artist', 'CCLIArtistCredits']: + author = root.get(author_key) + if author and len(author) > 0: + self.parse_author(author) + + # ProPresenter 4 + if(self.version >= 400 and self.version < 500): + if self.title.endswith('.pro4'): + self.title = self.title[:-5] + self.copyright = root.get('CCLICopyrightInfo') + self.ccli_number = root.get('CCLILicenseNumber') + count = 0 + for slide in root.slides.RVDisplaySlide: + count += 1 + if not hasattr(slide.displayElements, 'RVTextElement'): + log.debug('No text found, may be an image slide') + continue + RTFData = slide.displayElements.RVTextElement.get('RTFData') + rtf = base64.standard_b64decode(RTFData) + words, encoding = strip_rtf(rtf.decode()) + self.add_verse(words, "v%d" % count) + + # ProPresenter 5 + elif(self.version >= 500 and self.version < 600): + if self.title.endswith('.pro5'): + self.title = self.title[:-5] + self.copyright = root.get('CCLICopyrightInfo') + self.ccli_number = root.get('CCLILicenseNumber') + count = 0 + for group in root.groups.RVSlideGrouping: + for slide in group.slides.RVDisplaySlide: + count += 1 + if not hasattr(slide.displayElements, 'RVTextElement'): + log.debug('No text found, may be an image slide') + continue + RTFData = slide.displayElements.RVTextElement.get('RTFData') + rtf = base64.standard_b64decode(RTFData) + words, encoding = strip_rtf(rtf.decode()) + self.add_verse(words, "v%d" % count) + + # ProPresenter 6 + elif(self.version >= 600 and self.version < 700): + if self.title.endswith('.pro6'): + self.title = self.title[:-5] + self.copyright = root.get('CCLICopyrightYear') + self.ccli_number = root.get('CCLISongNumber') + count = 0 + for group in root.array.RVSlideGrouping: + for slide in group.array.RVDisplaySlide: + count += 1 + for item in slide.array: + if not (item.get('rvXMLIvarName')=="displayElements"): + continue + if not hasattr(item, 'RVTextElement'): + log.debug('No text found, may be an image slide') + continue + for contents in item.RVTextElement.NSString: + b64Data = contents.text + data = base64.standard_b64decode(b64Data) + words = None + if(contents.get('rvXMLIvarName')=="RTFData"): + words, encoding = strip_rtf(data.decode()) + break + if words: + self.add_verse(words, "v%d" % count) + + if not self.finish(): + self.log_error(self.import_source) diff --git a/tests/functional/openlp_plugins/songs/test_propresenterimport.py b/tests/functional/openlp_plugins/songs/test_propresenterimport.py index bb6cb2bf9..79735cdfe 100644 --- a/tests/functional/openlp_plugins/songs/test_propresenterimport.py +++ b/tests/functional/openlp_plugins/songs/test_propresenterimport.py @@ -39,9 +39,23 @@ class TestProPresenterFileImport(SongImportTestHelper): self.importer_module_name = 'propresenter' super(TestProPresenterFileImport, self).__init__(*args, **kwargs) - def test_song_import(self): + def test_pro4_song_import(self): """ - Test that loading a ProPresenter file works correctly + Test that loading a ProPresenter 4 file works correctly """ self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.pro4')], self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) + + def test_pro5_song_import(self): + """ + Test that loading a ProPresenter 5 file works correctly + """ + self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.pro5')], + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) + + def test_pro6_song_import(self): + """ + Test that loading a ProPresenter 6 file works correctly + """ + self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.pro6')], + self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) diff --git a/tests/resources/propresentersongs/Amazing Grace.pro5 b/tests/resources/propresentersongs/Amazing Grace.pro5 new file mode 100644 index 000000000..a19b51df9 --- /dev/null +++ b/tests/resources/propresentersongs/Amazing Grace.pro5 @@ -0,0 +1,520 @@ + + + + + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + <_-RVRect3D-_position x="10" y="10" z="0" width="1004" height="748" /> + <_-D-_serializedShadow containerClass="NSMutableDictionary"> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + + + + + + + + + + + <_-RVProTransitionObject-_transitionObject transitionType="-1" transitionDuration="1" motionEnabled="0" motionSpeed="0" /> + \ No newline at end of file diff --git a/tests/resources/propresentersongs/Amazing Grace.pro6 b/tests/resources/propresentersongs/Amazing Grace.pro6 new file mode 100644 index 000000000..d6eb39cbf --- /dev/null +++ b/tests/resources/propresentersongs/Amazing Grace.pro6 @@ -0,0 +1,490 @@ + + + + + + + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QW1hemluZyBncmFjZSEgSG93IHN3ZWV0IHRoZSBzb3VuZA== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBbWF6aW5nIGdyYWNlISBIb3cgc3dlZXQgdGhlIHNvdW5kfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BbWF6aW5nIGdyYWNlISBIb3cgc3dlZXQgdGhlIHNvdW5kPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhhdCBzYXZlZCBhIHdyZXRjaCBsaWtlIG1lIQ== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGF0IHNhdmVkIGEgd3JldGNoIGxpa2UgbWUhfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGF0IHNhdmVkIGEgd3JldGNoIGxpa2UgbWUhPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SSBvbmNlIHdhcyBsb3N0LCBidXQgbm93IGFtIGZvdW5kOw== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBJIG9uY2Ugd2FzIGxvc3QsIGJ1dCBub3cgYW0gZm91bmQ7fVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5JIG9uY2Ugd2FzIGxvc3QsIGJ1dCBub3cgYW0gZm91bmQ7PC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + V2FzIGJsaW5kLCBidXQgbm93IEkgc2VlLg== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBXYXMgYmxpbmQsIGJ1dCBub3cgSSBzZWUufVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5XYXMgYmxpbmQsIGJ1dCBub3cgSSBzZWUuPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + J1R3YXMgZ3JhY2UgdGhhdCB0YXVnaHQgbXkgaGVhcnQgdG8gZmVhciw= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCAnVHdhcyBncmFjZSB0aGF0IHRhdWdodCBteSBoZWFydCB0byBmZWFyLH1cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj4nVHdhcyBncmFjZSB0aGF0IHRhdWdodCBteSBoZWFydCB0byBmZWFyLDwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QW5kIGdyYWNlIG15IGZlYXJzIHJlbGlldmVkOw== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBbmQgZ3JhY2UgbXkgZmVhcnMgcmVsaWV2ZWQ7fVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BbmQgZ3JhY2UgbXkgZmVhcnMgcmVsaWV2ZWQ7PC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SG93IHByZWNpb3VzIGRpZCB0aGF0IGdyYWNlIGFwcGVhcg== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBIb3cgcHJlY2lvdXMgZGlkIHRoYXQgZ3JhY2UgYXBwZWFyfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5Ib3cgcHJlY2lvdXMgZGlkIHRoYXQgZ3JhY2UgYXBwZWFyPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhlIGhvdXIgSSBmaXJzdCBiZWxpZXZlZC4= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGUgaG91ciBJIGZpcnN0IGJlbGlldmVkLn1cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGUgaG91ciBJIGZpcnN0IGJlbGlldmVkLjwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhyb3VnaCBtYW55IGRhbmdlcnMsIHRvaWxzIGFuZCBzbmFyZXMs + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaHJvdWdoIG1hbnkgZGFuZ2VycywgdG9pbHMgYW5kIHNuYXJlcyx9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaHJvdWdoIG1hbnkgZGFuZ2VycywgdG9pbHMgYW5kIHNuYXJlcyw8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SSBoYXZlIGFscmVhZHkgY29tZTs= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBJIGhhdmUgYWxyZWFkeSBjb21lO31cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5JIGhhdmUgYWxyZWFkeSBjb21lOzwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + J1RpcyBncmFjZSBoYXRoIGJyb3VnaHQgbWUgc2FmZSB0aHVzIGZhciw= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCAnVGlzIGdyYWNlIGhhdGggYnJvdWdodCBtZSBzYWZlIHRodXMgZmFyLH1cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj4nVGlzIGdyYWNlIGhhdGggYnJvdWdodCBtZSBzYWZlIHRodXMgZmFyLDwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QW5kIGdyYWNlIHdpbGwgbGVhZCBtZSBob21lLg== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBbmQgZ3JhY2Ugd2lsbCBsZWFkIG1lIGhvbWUufVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BbmQgZ3JhY2Ugd2lsbCBsZWFkIG1lIGhvbWUuPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhlIExvcmQgaGFzIHByb21pc2VkIGdvb2QgdG8gbWUs + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGUgTG9yZCBoYXMgcHJvbWlzZWQgZ29vZCB0byBtZSx9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGUgTG9yZCBoYXMgcHJvbWlzZWQgZ29vZCB0byBtZSw8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SGlzIFdvcmQgbXkgaG9wZSBzZWN1cmVzOw== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBIaXMgV29yZCBteSBob3BlIHNlY3VyZXM7fVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5IaXMgV29yZCBteSBob3BlIHNlY3VyZXM7PC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SGUgd2lsbCBteSBTaGllbGQgYW5kIFBvcnRpb24gYmUs + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBIZSB3aWxsIG15IFNoaWVsZCBhbmQgUG9ydGlvbiBiZSx9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5IZSB3aWxsIG15IFNoaWVsZCBhbmQgUG9ydGlvbiBiZSw8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QXMgbG9uZyBhcyBsaWZlIGVuZHVyZXMu + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBcyBsb25nIGFzIGxpZmUgZW5kdXJlcy59XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BcyBsb25nIGFzIGxpZmUgZW5kdXJlcy48L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + WWVhLCB3aGVuIHRoaXMgZmxlc2ggYW5kIGhlYXJ0IHNoYWxsIGZhaWws + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBZZWEsIHdoZW4gdGhpcyBmbGVzaCBhbmQgaGVhcnQgc2hhbGwgZmFpbCx9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5ZZWEsIHdoZW4gdGhpcyBmbGVzaCBhbmQgaGVhcnQgc2hhbGwgZmFpbCw8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QW5kIG1vcnRhbCBsaWZlIHNoYWxsIGNlYXNlLA== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBbmQgbW9ydGFsIGxpZmUgc2hhbGwgY2Vhc2UsfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BbmQgbW9ydGFsIGxpZmUgc2hhbGwgY2Vhc2UsPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + SSBzaGFsbCBwb3NzZXNzLCB3aXRoaW4gdGhlIHZlaWws + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBJIHNoYWxsIHBvc3Nlc3MsIHdpdGhpbiB0aGUgdmVpbCx9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5JIHNoYWxsIHBvc3Nlc3MsIHdpdGhpbiB0aGUgdmVpbCw8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QSBsaWZlIG9mIGpveSBhbmQgcGVhY2Uu + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBBIGxpZmUgb2Ygam95IGFuZCBwZWFjZS59XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5BIGxpZmUgb2Ygam95IGFuZCBwZWFjZS48L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhlIGVhcnRoIHNoYWxsIHNvb24gZGlzc29sdmUgbGlrZSBzbm93LA== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGUgZWFydGggc2hhbGwgc29vbiBkaXNzb2x2ZSBsaWtlIHNub3csfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGUgZWFydGggc2hhbGwgc29vbiBkaXNzb2x2ZSBsaWtlIHNub3csPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhlIHN1biBmb3JiZWFyIHRvIHNoaW5lOw== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGUgc3VuIGZvcmJlYXIgdG8gc2hpbmU7fVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGUgc3VuIGZvcmJlYXIgdG8gc2hpbmU7PC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QnV0IEdvZCwgV2hvIGNhbGxlZCBtZSBoZXJlIGJlbG93LA== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBCdXQgR29kLCBXaG8gY2FsbGVkIG1lIGhlcmUgYmVsb3csfVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5CdXQgR29kLCBXaG8gY2FsbGVkIG1lIGhlcmUgYmVsb3csPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + U2hhbGwgYmUgZm9yZXZlciBtaW5lLg== + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBTaGFsbCBiZSBmb3JldmVyIG1pbmUufVxsaTBcc2EwXHNiMFxmaTBccWNccGFyfQ0KfQ0KfQ== + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5TaGFsbCBiZSBmb3JldmVyIG1pbmUuPC9SdW4+PC9TcGFuPjwvUGFyYWdyYXBoPjwvRmxvd0RvY3VtZW50Pg== + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + V2hlbiB3ZSd2ZSBiZWVuIHRoZXJlIHRlbiB0aG91c2FuZCB5ZWFycyw= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBXaGVuIHdlJ3ZlIGJlZW4gdGhlcmUgdGVuIHRob3VzYW5kIHllYXJzLH1cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5XaGVuIHdlJ3ZlIGJlZW4gdGhlcmUgdGVuIHRob3VzYW5kIHllYXJzLDwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + QnJpZ2h0IHNoaW5pbmcgYXMgdGhlIHN1biw= + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBCcmlnaHQgc2hpbmluZyBhcyB0aGUgc3VuLH1cbGkwXHNhMFxzYjBcZmkwXHFjXHBhcn0NCn0NCn0= + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5CcmlnaHQgc2hpbmluZyBhcyB0aGUgc3VuLDwvUnVuPjwvU3Bhbj48L1BhcmFncmFwaD48L0Zsb3dEb2N1bWVudD4= + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + V2UndmUgbm8gbGVzcyBkYXlzIHRvIHNpbmcgR29kJ3MgcHJhaXNl + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBXZSd2ZSBubyBsZXNzIGRheXMgdG8gc2luZyBHb2QncyBwcmFpc2V9XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5XZSd2ZSBubyBsZXNzIGRheXMgdG8gc2luZyBHb2QncyBwcmFpc2U8L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + {10 10 0 1004 748} + 0|0 0 0 0|{0, 0} + + 0 0 0 0 + 0 + + VGhhbiB3aGVuIHdlJ2QgZmlyc3QgYmVndW4u + e1xydGYxXHByb3J0ZjFcYW5zaVxhbnNpY3BnMTI1Mlx1YzFcaHRtYXV0c3BcZGVmZjJ7XGZvbnR0Ymx7XGYwXGZjaGFyc2V0MCBUaW1lcyBOZXcgUm9tYW47fXtcZjJcZmNoYXJzZXQwIEdlb3JnaWE7fXtcZjNcZmNoYXJzZXQwIEhlbHZldGljYTt9fXtcY29sb3J0Ymw7XHJlZDBcZ3JlZW4wXGJsdWUwO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTt9XGxvY2hcaGljaFxkYmNoXHBhcmRcc2xsZWFkaW5nMFxwbGFpblxsdHJwYXJcaXRhcDB7XGxhbmcxMDMzXGZzMTIwXGYzXGNmMSBcY2YxXHFse1xmMyB7XGNmMlxsdHJjaCBUaGFuIHdoZW4gd2UnZCBmaXJzdCBiZWd1bi59XGxpMFxzYTBcc2IwXGZpMFxxY1xwYXJ9DQp9DQp9 + PEZsb3dEb2N1bWVudCBUZXh0QWxpZ25tZW50PSJMZWZ0IiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwvcHJlc2VudGF0aW9uIj48UGFyYWdyYXBoIE1hcmdpbj0iMCwwLDAsMCIgVGV4dEFsaWdubWVudD0iQ2VudGVyIiBGb250RmFtaWx5PSJIZWx2ZXRpY2EiIEZvbnRTaXplPSI2MCI+PFNwYW4gRm9yZWdyb3VuZD0iI0ZGRkZGRkZGIiB4bWw6bGFuZz0iZW4tdXMiPjxSdW4gQmxvY2suVGV4dEFsaWdubWVudD0iQ2VudGVyIj5UaGFuIHdoZW4gd2UnZCBmaXJzdCBiZWd1bi48L1J1bj48L1NwYW4+PC9QYXJhZ3JhcGg+PC9GbG93RG9jdW1lbnQ+ + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+PFJWRm9udCB4bWxuczppPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9Qcm9QcmVzZW50ZXIuQ29tbW9uIj48S2VybmluZz4wPC9LZXJuaW5nPjxMaW5lU3BhY2luZz4wPC9MaW5lU3BhY2luZz48T3V0bGluZUNvbG9yIHhtbG5zOmQycDE9Imh0dHA6Ly9zY2hlbWFzLmRhdGFjb250cmFjdC5vcmcvMjAwNC8wNy9TeXN0ZW0uV2luZG93cy5NZWRpYSI+PGQycDE6QT4wPC9kMnAxOkE+PGQycDE6Qj4wPC9kMnAxOkI+PGQycDE6Rz4wPC9kMnAxOkc+PGQycDE6Uj4wPC9kMnAxOlI+PGQycDE6U2NBPjA8L2QycDE6U2NBPjxkMnAxOlNjQj4wPC9kMnAxOlNjQj48ZDJwMTpTY0c+MDwvZDJwMTpTY0c+PGQycDE6U2NSPjA8L2QycDE6U2NSPjwvT3V0bGluZUNvbG9yPjxPdXRsaW5lV2lkdGg+MDwvT3V0bGluZVdpZHRoPjxWYXJpYW50cz5Ob3JtYWw8L1ZhcmlhbnRzPjwvUlZGb250Pg== + + + + + + + + \ No newline at end of file From 9516588f621f0d0085ea37ff433c3fb75bfdbee6 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Thu, 31 Mar 2016 03:50:33 +1030 Subject: [PATCH 17/23] Housekeeping, pep8 errors --- .../songs/lib/importers/propresenter.py | 51 +++---------------- 1 file changed, 8 insertions(+), 43 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index b46f71a08..93db821c5 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -35,44 +35,6 @@ from .songimport import SongImport log = logging.getLogger(__name__) -''' -class ProPresenterImport(SongImport): - """ - The :class:`ProPresenterImport` class provides OpenLP with the - ability to import ProPresenter 4 song files. - """ - def do_import(self): - self.import_wizard.progress_bar.setMaximum(len(self.import_source)) - for file_path in self.import_source: - if self.stop_import_flag: - return - self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % os.path.basename(file_path)) - root = objectify.parse(open(file_path, 'rb')).getroot() - self.process_song(root, file_path) - - def process_song(self, root, filename): - self.set_defaults() - self.title = os.path.basename(filename).rstrip('.pro4') - self.copyright = root.get('CCLICopyrightInfo') - self.comments = root.get('notes') - self.ccli_number = root.get('CCLILicenseNumber') - for author_key in ['author', 'artist', 'CCLIArtistCredits']: - author = root.get(author_key) - if len(author) > 0: - self.parse_author(author) - count = 0 - for slide in root.slides.RVDisplaySlide: - count += 1 - if not hasattr(slide.displayElements, 'RVTextElement'): - log.debug('No text found, may be an image slide') - continue - RTFData = slide.displayElements.RVTextElement.get('RTFData') - rtf = base64.standard_b64decode(RTFData) - words, encoding = strip_rtf(rtf.decode()) - self.add_verse(words, "v%d" % count) - if not self.finish(): - self.log_error(self.import_source) -''' class ProPresenterImport(SongImport): """ @@ -90,14 +52,17 @@ class ProPresenterImport(SongImport): def process_song(self, root, filename): self.set_defaults() - self.comments = root.get('notes') self.title = os.path.basename(filename) + + # Extract ProPresenter versionNumber try: self.version = int(root.get('versionNumber')) except ValueError: - self.log_error(self.import_source) + log.debug('ProPresenter versionNumber invalid or missing') return + # Common settings + self.comments = root.get('notes') for author_key in ['author', 'CCLIAuthor', 'artist', 'CCLIArtistCredits']: author = root.get(author_key) if author and len(author) > 0: @@ -149,7 +114,7 @@ class ProPresenterImport(SongImport): for slide in group.array.RVDisplaySlide: count += 1 for item in slide.array: - if not (item.get('rvXMLIvarName')=="displayElements"): + if not (item.get('rvXMLIvarName') == "displayElements"): continue if not hasattr(item, 'RVTextElement'): log.debug('No text found, may be an image slide') @@ -158,11 +123,11 @@ class ProPresenterImport(SongImport): b64Data = contents.text data = base64.standard_b64decode(b64Data) words = None - if(contents.get('rvXMLIvarName')=="RTFData"): + if(contents.get('rvXMLIvarName') == "RTFData"): words, encoding = strip_rtf(data.decode()) break if words: self.add_verse(words, "v%d" % count) - + if not self.finish(): self.log_error(self.import_source) From 184b0e538a8f631a5e43ea3f8fdd334222a7c80f Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Thu, 31 Mar 2016 16:09:13 +1030 Subject: [PATCH 18/23] Implemented recommended changes --- openlp/plugins/songs/lib/importer.py | 2 +- .../songs/lib/importers/propresenter.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 8a79ea04f..47f6edb46 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -313,7 +313,7 @@ class SongFormat(object): }, ProPresenter: { 'class': ProPresenterImport, - 'name': 'ProPresenter 4 -> 6', + 'name': 'ProPresenter 4, 5 and 6', 'prefix': 'proPresenter', 'filter': '%s (*.pro4 *.pro5 *.pro6)' % translate('SongsPlugin.ImportWizardForm', 'ProPresenter Song Files') }, diff --git a/openlp/plugins/songs/lib/importers/propresenter.py b/openlp/plugins/songs/lib/importers/propresenter.py index 93db821c5..cddf0e52b 100644 --- a/openlp/plugins/songs/lib/importers/propresenter.py +++ b/openlp/plugins/songs/lib/importers/propresenter.py @@ -39,7 +39,7 @@ log = logging.getLogger(__name__) class ProPresenterImport(SongImport): """ The :class:`ProPresenterImport` class provides OpenLP with the - ability to import ProPresenter *4-6* song files. + ability to import ProPresenter 4-6 song files. """ def do_import(self): self.import_wizard.progress_bar.setMaximum(len(self.import_source)) @@ -52,7 +52,6 @@ class ProPresenterImport(SongImport): def process_song(self, root, filename): self.set_defaults() - self.title = os.path.basename(filename) # Extract ProPresenter versionNumber try: @@ -61,8 +60,15 @@ class ProPresenterImport(SongImport): log.debug('ProPresenter versionNumber invalid or missing') return - # Common settings + # Title + self.title = root.get('CCLISongTitle') + if not self.title or self.title == '': + self.title = os.path.basename(filename) + if self.title[-5:-1] == '.pro': + self.title = self.title[:-5] + # Notes self.comments = root.get('notes') + # Author for author_key in ['author', 'CCLIAuthor', 'artist', 'CCLIArtistCredits']: author = root.get(author_key) if author and len(author) > 0: @@ -70,8 +76,6 @@ class ProPresenterImport(SongImport): # ProPresenter 4 if(self.version >= 400 and self.version < 500): - if self.title.endswith('.pro4'): - self.title = self.title[:-5] self.copyright = root.get('CCLICopyrightInfo') self.ccli_number = root.get('CCLILicenseNumber') count = 0 @@ -87,8 +91,6 @@ class ProPresenterImport(SongImport): # ProPresenter 5 elif(self.version >= 500 and self.version < 600): - if self.title.endswith('.pro5'): - self.title = self.title[:-5] self.copyright = root.get('CCLICopyrightInfo') self.ccli_number = root.get('CCLILicenseNumber') count = 0 @@ -105,8 +107,6 @@ class ProPresenterImport(SongImport): # ProPresenter 6 elif(self.version >= 600 and self.version < 700): - if self.title.endswith('.pro6'): - self.title = self.title[:-5] self.copyright = root.get('CCLICopyrightYear') self.ccli_number = root.get('CCLISongNumber') count = 0 From 6bca1fc45599bd13d431a86cdc83ef74c62f1fc1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 1 Apr 2016 20:04:15 -0700 Subject: [PATCH 19/23] Fix decode() string error in about text - fix qt try/except error in projector --- openlp/core/lib/projector/pjlink1.py | 18 +++++++----------- openlp/core/utils/__init__.py | 4 ++-- .../openlp_core_lib/test_projector_pjlink1.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index c5c765d62..4cdd31269 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -513,17 +513,13 @@ class PJLink1(QTcpSocket): log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip())) log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue)) self.socket_timer.start() - try: - self.projectorNetwork.emit(S_NETWORK_SENDING) - sent = self.write(out.encode('ascii')) - self.waitForBytesWritten(2000) # 2 seconds should be enough - if sent == -1: - # Network error? - self.change_status(E_NETWORK, - translate('OpenLP.PJLink1', 'Error while sending data to projector')) - except SocketError as e: - self.disconnect_from_host(abort=True) - self.changeStatus(E_NETWORK, '%s : %s' % (e.error(), e.errorString())) + self.projectorNetwork.emit(S_NETWORK_SENDING) + sent = self.write(out.encode('ascii')) + self.waitForBytesWritten(2000) # 2 seconds should be enough + if sent == -1: + # Network error? + self.change_status(E_NETWORK, + translate('OpenLP.PJLink1', 'Error while sending data to projector')) def process_command(self, cmd, data): """ diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 6829383de..8b09b18a2 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -186,9 +186,9 @@ def get_application_version(): # If they are equal, then this tree is tarball with the source for the release. We do not want the revision # number in the full version. if tree_revision == tag_revision: - full_version = tag_version.decode('utf-8') + full_version = tag_version.strip() else: - full_version = '%s-bzr%s' % (tag_version.decode('utf-8'), tree_revision.decode('utf-8')) + full_version = '%s-bzr%s' % (tag_version.strip(), tree_revision.strip()) else: # We're not running the development version, let's use the file. file_path = AppLocation.get_directory(AppLocation.VersionDir) diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 7e19ff065..a3d99e884 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -92,3 +92,18 @@ class TestPJLink(TestCase): mock_change_status.called_with(E_PARAMETER, 'change_status should have been called with "{}"'.format( ERROR_STRING[E_PARAMETER])) + + @patch.object(pjlink_test, 'process_inpt') + def projector_return_ok_test(self, mock_process_inpt): + """ + Test projector calls process_inpt command when process_command is called with INPT option + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: process_command is called with INST command and 31 input: + pjlink.process_command('INPT', '31') + + # THEN: process_inpt method should have been called with 31 + mock_process_inpt.called_with('31', + "process_inpt should have been called with 31") From 330a1758c8a1295a468f458383ed5aa2882bb98a Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 3 Apr 2016 11:57:39 +0100 Subject: [PATCH 20/23] Use get_natural_key instead of _natural_sort_key --- openlp/core/utils/__init__.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 25 ++++++++----------- .../openlp_plugins/songs/test_mediaitem.py | 13 ---------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 086c69c79..69187ed74 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -529,7 +529,7 @@ def get_natural_key(string): key = [int(part) if part.isdigit() else get_locale_key(part) for part in key] # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str # and int. - if string[0].isdigit(): + if string and string[0].isdigit(): return [b''] + key return key diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index e46b95fbf..deefa2acd 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -32,6 +32,7 @@ from openlp.core.common import Registry, AppLocation, Settings, check_directory_ from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ check_item_selected, create_separated_list from openlp.core.lib.ui import create_widget_action +from openlp.core.utils import get_natural_key from openlp.plugins.songs.forms.editsongform import EditSongForm from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm @@ -284,8 +285,10 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Author') self.list_view.clear() + search_results = sorted(search_results, key=lambda author: (get_natural_key(author.display_name))) for author in search_results: - for song in author.songs: + songs = sorted(author.songs, key=lambda song: song.sort_key) + for song in songs: # Do not display temporary songs if song.temporary: continue @@ -303,8 +306,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, - self._natural_sort_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: (get_natural_key(songbook_entry.songbook.name), + get_natural_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue @@ -322,7 +325,7 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Topic') self.list_view.clear() - search_results = sorted(search_results, key=lambda topic: self._natural_sort_key(topic.name)) + search_results = sorted(search_results, key=lambda topic: get_natural_key(topic.name)) for topic in search_results: songs = sorted(topic.songs, key=lambda song: song.sort_key) for song in songs: @@ -343,6 +346,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Themes') self.list_view.clear() + search_results = sorted(search_results, key=lambda song: (get_natural_key(song.theme_name), + song.sort_key)) for song in search_results: # Do not display temporary songs if song.temporary: @@ -361,8 +366,8 @@ class SongMediaItem(MediaManagerItem): """ 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: + search_results = sorted(search_results, key=lambda song: get_natural_key(song.ccli_number)) + for song in search_results: # Do not display temporary songs if song.temporary: continue @@ -688,14 +693,6 @@ 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 diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 5e343fc9d..3cd5f97ba 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -448,19 +448,6 @@ 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 From efb71b4f3125033704b43d555ae59341db899504 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 3 Apr 2016 11:58:35 +0100 Subject: [PATCH 21/23] Merge w/ trunk --- openlp/plugins/songs/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index deefa2acd..2644ef125 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -285,7 +285,7 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Author') self.list_view.clear() - search_results = sorted(search_results, key=lambda author: (get_natural_key(author.display_name))) + search_results = sorted(search_results, key=lambda author: get_natural_key(author.display_name)) for author in search_results: songs = sorted(author.songs, key=lambda song: song.sort_key) for song in songs: From ae394cf028539e2ba73fab8a955218960083ed9d Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 3 Apr 2016 12:02:53 +0100 Subject: [PATCH 22/23] Minor fixes --- openlp/plugins/songs/lib/mediaitem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 2644ef125..7842d7cfc 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -366,8 +366,9 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results CCLI number') self.list_view.clear() - search_results = sorted(search_results, key=lambda song: get_natural_key(song.ccli_number)) - for song in search_results: + songs = sorted(search_results, key=lambda song: (get_natural_key(song.ccli_number), + song.sort_key)) + for song in songs: # Do not display temporary songs if song.temporary: continue From 33cbda294d290f95d297ea6600298e7cdd0b8254 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 3 Apr 2016 12:14:17 +0100 Subject: [PATCH 23/23] Coding standards fixes --- openlp/plugins/songs/lib/mediaitem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 7842d7cfc..b84e557c2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -306,8 +306,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: (get_natural_key(songbook_entry.songbook.name), - get_natural_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: + (get_natural_key(songbook_entry.songbook.name), get_natural_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue @@ -367,7 +367,7 @@ class SongMediaItem(MediaManagerItem): log.debug('display results CCLI number') self.list_view.clear() songs = sorted(search_results, key=lambda song: (get_natural_key(song.ccli_number), - song.sort_key)) + song.sort_key)) for song in songs: # Do not display temporary songs if song.temporary: