From c8f200b20fd77b5b4486a1306f1bdd96e2cd9ea4 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Fri, 13 Feb 2015 18:29:42 +0000 Subject: [PATCH 01/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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/55] 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 203c7b9dd8b5673f593f72ea0615cc8d6152d025 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 1 Apr 2016 17:56:54 +0100 Subject: [PATCH 19/55] move historycombo --- openlp/core/common/__init__.py | 6 ++---- openlp/core/ui/lib/__init__.py | 21 +++++++++++++++++++ .../{common => ui/lib}/historycombobox.py | 2 +- .../interfaces/openlp_core_ui_lib/__init__.py | 21 +++++++++++++++++++ .../test_historycombobox.py | 3 +-- 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openlp/core/ui/lib/__init__.py rename openlp/core/{common => ui/lib}/historycombobox.py (98%) create mode 100644 tests/interfaces/openlp_core_ui_lib/__init__.py rename tests/interfaces/{openlp_core_common => openlp_core_ui_lib}/test_historycombobox.py (96%) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index fcb96e5ac..e891e5502 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -24,13 +24,12 @@ The :mod:`common` module contains most of the components and libraries that make OpenLP work. """ import hashlib -import re -import os import logging +import os +import re import sys import traceback from ipaddress import IPv4Address, IPv6Address, AddressValueError -from codecs import decode, encode from PyQt5 import QtCore from PyQt5.QtCore import QCryptographicHash as QHash @@ -241,6 +240,5 @@ from .registryproperties import RegistryProperties from .uistrings import UiStrings from .settings import Settings from .applocation import AppLocation -from .historycombobox import HistoryComboBox from .actions import ActionList from .languagemanager import LanguageManager diff --git a/openlp/core/ui/lib/__init__.py b/openlp/core/ui/lib/__init__.py new file mode 100644 index 000000000..ef0211030 --- /dev/null +++ b/openlp/core/ui/lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### \ No newline at end of file diff --git a/openlp/core/common/historycombobox.py b/openlp/core/ui/lib/historycombobox.py similarity index 98% rename from openlp/core/common/historycombobox.py rename to openlp/core/ui/lib/historycombobox.py index f0ec7c2ad..23e05e76e 100644 --- a/openlp/core/common/historycombobox.py +++ b/openlp/core/ui/lib/historycombobox.py @@ -20,7 +20,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -The :mod:`~openlp.core.common.historycombobox` module contains the HistoryComboBox widget +The :mod:`~openlp.core.ui.lib.historycombobox` module contains the HistoryComboBox widget """ from PyQt5 import QtCore, QtWidgets diff --git a/tests/interfaces/openlp_core_ui_lib/__init__.py b/tests/interfaces/openlp_core_ui_lib/__init__.py new file mode 100644 index 000000000..ef0211030 --- /dev/null +++ b/tests/interfaces/openlp_core_ui_lib/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### \ No newline at end of file diff --git a/tests/interfaces/openlp_core_common/test_historycombobox.py b/tests/interfaces/openlp_core_ui_lib/test_historycombobox.py similarity index 96% rename from tests/interfaces/openlp_core_common/test_historycombobox.py rename to tests/interfaces/openlp_core_ui_lib/test_historycombobox.py index 1bc7a2f0d..7aa460c8f 100644 --- a/tests/interfaces/openlp_core_common/test_historycombobox.py +++ b/tests/interfaces/openlp_core_ui_lib/test_historycombobox.py @@ -28,9 +28,8 @@ from unittest import TestCase from PyQt5 import QtWidgets from openlp.core.common import Registry -from openlp.core.common import HistoryComboBox +from openlp.core.ui.lib.historycombobox import HistoryComboBox from tests.helpers.testmixin import TestMixin -from tests.interfaces import MagicMock, patch class TestHistoryComboBox(TestCase, TestMixin): From abd2c16ccd5f1b76f2c80c5afc90600aa1a38dbb Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 1 Apr 2016 18:02:14 +0100 Subject: [PATCH 20/55] fix links --- openlp/plugins/songs/forms/songselectdialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/forms/songselectdialog.py b/openlp/plugins/songs/forms/songselectdialog.py index 68d91e1ae..833ee39ec 100644 --- a/openlp/plugins/songs/forms/songselectdialog.py +++ b/openlp/plugins/songs/forms/songselectdialog.py @@ -25,7 +25,7 @@ The :mod:`~openlp.plugins.songs.forms.songselectdialog` module contains the user from PyQt5 import QtCore, QtWidgets -from openlp.core.common import HistoryComboBox +from openlp.core.ui.lib.historycombobox import HistoryComboBox from openlp.core.lib import translate, build_icon from openlp.core.ui import SingleColumnTableWidget From 06781554003ce0a7a64147f6ebf2f148e9d181fa Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 1 Apr 2016 18:09:47 +0100 Subject: [PATCH 21/55] fix PEP8 --- openlp/core/ui/lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/lib/__init__.py b/openlp/core/ui/lib/__init__.py index ef0211030..02bded5b0 100644 --- a/openlp/core/ui/lib/__init__.py +++ b/openlp/core/ui/lib/__init__.py @@ -18,4 +18,4 @@ # You should have received a copy of the GNU General Public License along # # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### \ No newline at end of file +############################################################################### From dbb07db257d6f446ec73795f4e1534760d935353 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 1 Apr 2016 18:15:45 +0100 Subject: [PATCH 22/55] fix PEP8 --- openlp/core/utils/__init__.py | 4 ++-- tests/interfaces/openlp_core_ui_lib/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 6829383de..72fe6c138 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -450,7 +450,7 @@ def get_web_page(url, header=None, update_openlp=False): def get_uno_command(connection_type='pipe'): """ - Returns the UNO command to launch an openoffice.org instance. + Returns the UNO command to launch an libreoffice.org instance. """ for command in ['libreoffice', 'soffice']: if which(command): @@ -468,7 +468,7 @@ def get_uno_command(connection_type='pipe'): def get_uno_instance(resolver, connection_type='pipe'): """ - Returns a running openoffice.org instance. + Returns a running libreoffice.org instance. :param resolver: The UNO resolver to use to find a running instance. """ diff --git a/tests/interfaces/openlp_core_ui_lib/__init__.py b/tests/interfaces/openlp_core_ui_lib/__init__.py index ef0211030..02bded5b0 100644 --- a/tests/interfaces/openlp_core_ui_lib/__init__.py +++ b/tests/interfaces/openlp_core_ui_lib/__init__.py @@ -18,4 +18,4 @@ # You should have received a copy of the GNU General Public License along # # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### \ No newline at end of file +############################################################################### From ec30c560a7e7afbb0072dccb7e8377e4fa3f78b2 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 1 Apr 2016 18:28:40 +0100 Subject: [PATCH 23/55] move tests and code --- openlp/core/common/__init__.py | 15 +++ openlp/core/ui/mainwindow.py | 4 +- openlp/core/utils/__init__.py | 17 +--- .../openlp_core_common/test_init.py | 95 +++++++++++++++++++ .../openlp_core_utils/test_utils.py | 62 +----------- 5 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 tests/functional/openlp_core_common/test_init.py diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index e891e5502..b382a719f 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -242,3 +242,18 @@ from .settings import Settings from .applocation import AppLocation from .actions import ActionList from .languagemanager import LanguageManager + + +def add_actions(target, actions): + """ + Adds multiple actions to a menu or toolbar in one command. + + :param target: The menu or toolbar to add actions to + :param actions: The actions to be added. An action consisting of the keyword ``None`` + will result in a separator being inserted into the target. + """ + for action in actions: + if action is None: + target.addSeparator() + else: + target.addAction(action) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 3fd154c14..9618b1c48 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -35,7 +35,7 @@ from tempfile import gettempdir from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \ - check_directory_exists, translate, is_win, is_macosx + check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \ build_icon @@ -46,7 +46,7 @@ from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager -from openlp.core.utils import get_application_version, add_actions +from openlp.core.utils import get_application_version log = logging.getLogger(__name__) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 72fe6c138..68b807b3c 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -267,21 +267,6 @@ def check_latest_version(current_version): return version_string -def add_actions(target, actions): - """ - Adds multiple actions to a menu or toolbar in one command. - - :param target: The menu or toolbar to add actions to - :param actions: The actions to be added. An action consisting of the keyword ``None`` - will result in a separator being inserted into the target. - """ - for action in actions: - if action is None: - target.addSeparator() - else: - target.addAction(action) - - def get_filesystem_encoding(): """ Returns the name of the encoding used to convert Unicode filenames into system file names. @@ -535,5 +520,5 @@ def get_natural_key(string): return key __all__ = ['get_application_version', 'check_latest_version', - 'add_actions', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance', + 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance', 'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key'] diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py new file mode 100644 index 000000000..70fe6e943 --- /dev/null +++ b/tests/functional/openlp_core_common/test_init.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the AppLocation class and related methods. +""" +import os +from unittest import TestCase + +from openlp.core.common import add_actions +from tests.functional import MagicMock, patch + + +class TestInit(TestCase): + """ + A test suite to test out various methods around the common __init__ class. + """ + + def add_actions_empty_list_test(self): + """ + Test that no actions are added when the list is empty + """ + # GIVEN: a mocked action list, and an empty list + mocked_target = MagicMock() + empty_list = [] + + # WHEN: The empty list is added to the mocked target + add_actions(mocked_target, empty_list) + + # THEN: The add method on the mocked target is never called + self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') + self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + + def add_actions_none_action_test(self): + """ + Test that a separator is added when a None action is in the list + """ + # GIVEN: a mocked action list, and a list with None in it + mocked_target = MagicMock() + separator_list = [None] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, separator_list) + + # THEN: The addSeparator method is called, but the addAction method is never called + mocked_target.addSeparator.assert_called_with() + self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') + + def add_actions_add_action_test(self): + """ + Test that an action is added when a valid action is in the list + """ + # GIVEN: a mocked action list, and a list with an action in it + mocked_target = MagicMock() + action_list = ['action'] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, action_list) + + # THEN: The addSeparator method is not called, and the addAction method is called + self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') + mocked_target.addAction.assert_called_with('action') + + def add_actions_action_and_none_test(self): + """ + Test that an action and a separator are added when a valid action and None are in the list + """ + # GIVEN: a mocked action list, and a list with an action and None in it + mocked_target = MagicMock() + action_list = ['action', None] + + # WHEN: The list is added to the mocked target + add_actions(mocked_target, action_list) + + # THEN: The addSeparator method is called, and the addAction method is called + mocked_target.addSeparator.assert_called_with() + mocked_target.addAction.assert_called_with('action') diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index a9e402369..fd033cd88 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -26,7 +26,8 @@ import os from unittest import TestCase from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, get_locale_key, \ - get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance, add_actions + get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance + from tests.functional import MagicMock, patch @@ -34,65 +35,6 @@ class TestUtils(TestCase): """ A test suite to test out various methods around the AppLocation class. """ - def add_actions_empty_list_test(self): - """ - Test that no actions are added when the list is empty - """ - # GIVEN: a mocked action list, and an empty list - mocked_target = MagicMock() - empty_list = [] - - # WHEN: The empty list is added to the mocked target - add_actions(mocked_target, empty_list) - - # THEN: The add method on the mocked target is never called - self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') - self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') - - def add_actions_none_action_test(self): - """ - Test that a separator is added when a None action is in the list - """ - # GIVEN: a mocked action list, and a list with None in it - mocked_target = MagicMock() - separator_list = [None] - - # WHEN: The list is added to the mocked target - add_actions(mocked_target, separator_list) - - # THEN: The addSeparator method is called, but the addAction method is never called - mocked_target.addSeparator.assert_called_with() - self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called') - - def add_actions_add_action_test(self): - """ - Test that an action is added when a valid action is in the list - """ - # GIVEN: a mocked action list, and a list with an action in it - mocked_target = MagicMock() - action_list = ['action'] - - # WHEN: The list is added to the mocked target - add_actions(mocked_target, action_list) - - # THEN: The addSeparator method is not called, and the addAction method is called - self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called') - mocked_target.addAction.assert_called_with('action') - - def add_actions_action_and_none_test(self): - """ - Test that an action and a separator are added when a valid action and None are in the list - """ - # GIVEN: a mocked action list, and a list with an action and None in it - mocked_target = MagicMock() - action_list = ['action', None] - - # WHEN: The list is added to the mocked target - add_actions(mocked_target, action_list) - - # THEN: The addSeparator method is called, and the addAction method is called - mocked_target.addSeparator.assert_called_with() - mocked_target.addAction.assert_called_with('action') def get_filesystem_encoding_sys_function_not_called_test(self): """ From 6bca1fc45599bd13d431a86cdc83ef74c62f1fc1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 1 Apr 2016 20:04:15 -0700 Subject: [PATCH 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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: From d7e2e0e839b1687230df0949775a8a0f6ee50c98 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 3 Apr 2016 20:06:48 +0100 Subject: [PATCH 29/55] remove import --- openlp/core/common/languagemanager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index 873b64c57..4f5128425 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -24,7 +24,6 @@ The :mod:`languagemanager` module provides all the translation settings and lang """ import logging import re -import sys from PyQt5 import QtCore, QtWidgets From a0bfc7d0690cce3d089cc66b5020a014a9802733 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 3 Apr 2016 20:44:09 +0100 Subject: [PATCH 30/55] move methods --- openlp/core/common/languagemanager.py | 60 +++++++++++++++++ openlp/core/ui/advancedtab.py | 3 +- openlp/core/ui/servicemanager.py | 3 +- openlp/core/ui/thememanager.py | 3 +- openlp/core/utils/__init__.py | 59 +---------------- .../plugins/bibles/forms/bibleimportform.py | 2 +- openlp/plugins/bibles/lib/mediaitem.py | 2 +- openlp/plugins/custom/lib/db.py | 2 +- openlp/plugins/images/lib/mediaitem.py | 3 +- openlp/plugins/media/lib/mediaitem.py | 2 +- openlp/plugins/presentations/lib/mediaitem.py | 2 +- openlp/plugins/songs/lib/db.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 2 +- .../openlp_core_common/test_init.py | 3 +- .../test_languagemanager.py | 66 +++++++++++++++++++ .../openlp_core_utils/test_utils.py | 37 +---------- 16 files changed, 146 insertions(+), 105 deletions(-) create mode 100644 tests/functional/openlp_core_common/test_languagemanager.py diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index 4f5128425..c3e736d65 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -22,6 +22,7 @@ """ The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP. """ +import locale import logging import re @@ -32,6 +33,8 @@ from openlp.core.common import AppLocation, Settings, translate, is_win, is_maco log = logging.getLogger(__name__) +DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) + class LanguageManager(object): """ @@ -143,3 +146,60 @@ class LanguageManager(object): if not LanguageManager.__qm_list__: LanguageManager.init_qm_list() return LanguageManager.__qm_list__ + + +def format_time(text, local_time): + """ + Workaround for Python built-in time formatting function time.strftime(). + + time.strftime() accepts only ascii characters. This function accepts + unicode string and passes individual % placeholders to time.strftime(). + This ensures only ascii characters are passed to time.strftime(). + + :param text: The text to be processed. + :param local_time: The time to be used to add to the string. This is a time object + """ + + def match_formatting(match): + """ + Format the match + """ + return local_time.strftime(match.group()) + + return re.sub('\%[a-zA-Z]', match_formatting, text) + + +def get_locale_key(string): + """ + Creates a key for case insensitive, locale aware string sorting. + + :param string: The corresponding string. + """ + string = string.lower() + # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases. + global ICU_COLLATOR + try: + if ICU_COLLATOR is None: + import icu + language = LanguageManager.get_language() + icu_locale = icu.Locale(language) + ICU_COLLATOR = icu.Collator.createInstance(icu_locale) + return ICU_COLLATOR.getSortKey(string) + except: + return locale.strxfrm(string).encode() + + +def get_natural_key(string): + """ + Generate a key for locale aware natural string sorting. + + :param string: string to be sorted by + Returns a list of string compare keys and integers. + """ + key = DIGITS_OR_NONDIGITS.findall(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 and string[0].isdigit(): + return [b''] + key + return key diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index e2aadf280..ce26ae808 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -31,7 +31,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate from openlp.core.lib import ColorButton, SettingsTab, build_icon -from openlp.core.utils import format_time, get_images_filter +from openlp.core.common.languagemanager import format_time +from openlp.core.utils import get_images_filter log = logging.getLogger(__name__) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 6db34022b..129d24ca1 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -38,7 +38,8 @@ from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.lib import OpenLPToolbar, ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm -from openlp.core.utils import delete_file, split_filename, format_time +from openlp.core.utils import delete_file, split_filename +from openlp.core.common.languagemanager import format_time class ServiceManagerList(QtWidgets.QTreeWidget): diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index fdf87e528..fc632d61d 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -36,7 +36,8 @@ from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationEr from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.utils import delete_file, get_locale_key, get_filesystem_encoding +from openlp.core.utils import delete_file, get_filesystem_encoding +from openlp.core.common.languagemanager import get_locale_key class Ui_ThemeManager(object): diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 800eec557..3cd1b0295 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -22,7 +22,6 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ -import locale import logging import os import platform @@ -463,62 +462,6 @@ def get_uno_instance(resolver, connection_type='pipe'): else: return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') - -def format_time(text, local_time): - """ - Workaround for Python built-in time formatting function time.strftime(). - - time.strftime() accepts only ascii characters. This function accepts - unicode string and passes individual % placeholders to time.strftime(). - This ensures only ascii characters are passed to time.strftime(). - - :param text: The text to be processed. - :param local_time: The time to be used to add to the string. This is a time object - """ - def match_formatting(match): - """ - Format the match - """ - return local_time.strftime(match.group()) - return re.sub('\%[a-zA-Z]', match_formatting, text) - - -def get_locale_key(string): - """ - Creates a key for case insensitive, locale aware string sorting. - - :param string: The corresponding string. - """ - string = string.lower() - # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases. - global ICU_COLLATOR - try: - if ICU_COLLATOR is None: - import icu - from openlp.core.common.languagemanager import LanguageManager - language = LanguageManager.get_language() - icu_locale = icu.Locale(language) - ICU_COLLATOR = icu.Collator.createInstance(icu_locale) - return ICU_COLLATOR.getSortKey(string) - except: - return locale.strxfrm(string).encode() - - -def get_natural_key(string): - """ - Generate a key for locale aware natural string sorting. - - :param string: string to be sorted by - Returns a list of string compare keys and integers. - """ - key = DIGITS_OR_NONDIGITS.findall(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 and string[0].isdigit(): - return [b''] + key - return key - __all__ = ['get_application_version', 'check_latest_version', 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance', - 'delete_file', 'clean_filename', 'format_time', 'get_locale_key', 'get_natural_key'] + 'delete_file', 'clean_filename'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 52417f2ef..676801eb8 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -32,7 +32,7 @@ from openlp.core.common import AppLocation, Settings, UiStrings, translate from openlp.core.lib.db import delete_database from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.bibles.lib.manager import BibleFormat from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename from openlp.plugins.bibles.lib.http import CWExtract, BGExtract, BSExtract diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 94937e61b..cd728a68b 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -29,7 +29,7 @@ from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemConte from openlp.core.lib.searchedit import SearchEdit from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \ critical_error_message_box, find_and_set_in_combo_box, build_icon -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm from openlp.plugins.bibles.forms.editbibleform import EditBibleForm from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \ diff --git a/openlp/plugins/custom/lib/db.py b/openlp/plugins/custom/lib/db.py index 743822072..62ec1f408 100644 --- a/openlp/plugins/custom/lib/db.py +++ b/openlp/plugins/custom/lib/db.py @@ -28,7 +28,7 @@ from sqlalchemy import Column, Table, types from sqlalchemy.orm import mapper from openlp.core.lib.db import BaseModel, init_db -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key class CustomSlide(BaseModel): diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 248da94ea..c57442156 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -29,7 +29,8 @@ from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.utils import delete_file, get_locale_key, get_images_filter +from openlp.core.utils import delete_file, get_images_filter +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 09b32b308..dfe6f1fa4 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -32,7 +32,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.core.ui.media.vlcplayer import get_vlc if get_vlc() is not None: diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index b2ece3e67..b64c552b8 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -29,7 +29,7 @@ from openlp.core.common import Registry, Settings, UiStrings, translate from openlp.core.lib import MediaManagerItem, ItemCapabilities, ServiceItemContext,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box -from openlp.core.utils import get_locale_key +from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.presentations.lib import MessageListener from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 8fc6e1a4a..5ea35d6b6 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -29,7 +29,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql.expression import func, text from openlp.core.lib.db import BaseModel, init_db -from openlp.core.utils import get_natural_key +from openlp.core.common.languagemanager import get_natural_key from openlp.core.lib import translate diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index b84e557c2..d724bfaf2 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -32,7 +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.core.common.languagemanager 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 diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 70fe6e943..1df0bcedc 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -22,11 +22,10 @@ """ Functional tests to test the AppLocation class and related methods. """ -import os from unittest import TestCase from openlp.core.common import add_actions -from tests.functional import MagicMock, patch +from tests.functional import MagicMock class TestInit(TestCase): diff --git a/tests/functional/openlp_core_common/test_languagemanager.py b/tests/functional/openlp_core_common/test_languagemanager.py new file mode 100644 index 000000000..8fe7d543c --- /dev/null +++ b/tests/functional/openlp_core_common/test_languagemanager.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Functional tests to test the AppLocation class and related methods. +""" +from unittest import TestCase + +from tests.functional import patch +from openlp.core.common.languagemanager import get_locale_key, get_natural_key + + +class TestLanguageManager(TestCase): + """ + A test suite to test out various methods around the common __init__ class. + """ + + def get_locale_key_test(self): + """ + Test the get_locale_key(string) function + """ + with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language: + # GIVEN: The language is German + # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss". + mocked_get_language.return_value = 'de' + unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] + + # WHEN: We sort the list and use get_locale_key() to generate the sorting keys + sorted_list = sorted(unsorted_list, key=get_locale_key) + + # THEN: We get a properly sorted list + self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list, + 'Strings should be sorted properly') + + def get_natural_key_test(self): + """ + Test the get_natural_key(string) function + """ + with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language: + # GIVEN: The language is English (a language, which sorts digits before letters) + mocked_get_language.return_value = 'en' + unsorted_list = ['item 10a', 'item 3b', '1st item'] + + # WHEN: We sort the list and use get_natural_key() to generate the sorting keys + sorted_list = sorted(unsorted_list, key=get_natural_key) + + # THEN: We get a properly sorted list + self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally') diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index fd033cd88..0c4234a3d 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -25,8 +25,9 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, get_locale_key, \ - get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance +from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ + split_filename, _get_user_agent, get_web_page, get_uno_instance +from openlp.core.common.languagemanager import get_locale_key, get_natural_key from tests.functional import MagicMock, patch @@ -179,38 +180,6 @@ class TestUtils(TestCase): self.assertEqual(mocked_log.exception.call_count, 1) self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError') - def get_locale_key_test(self): - """ - Test the get_locale_key(string) function - """ - with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language: - # GIVEN: The language is German - # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss". - mocked_get_language.return_value = 'de' - unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] - - # WHEN: We sort the list and use get_locale_key() to generate the sorting keys - sorted_list = sorted(unsorted_list, key=get_locale_key) - - # THEN: We get a properly sorted list - self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list, - 'Strings should be sorted properly') - - def get_natural_key_test(self): - """ - Test the get_natural_key(string) function - """ - with patch('openlp.core.common.languagemanager.LanguageManager.get_language') as mocked_get_language: - # GIVEN: The language is English (a language, which sorts digits before letters) - mocked_get_language.return_value = 'en' - unsorted_list = ['item 10a', 'item 3b', '1st item'] - - # WHEN: We sort the list and use get_natural_key() to generate the sorting keys - sorted_list = sorted(unsorted_list, key=get_natural_key) - - # THEN: We get a properly sorted list - self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally') - def get_uno_instance_pipe_test(self): """ Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI From dffba47b4432d016bd88a132ce73cecd52e9f4e9 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 20:53:54 +0100 Subject: [PATCH 31/55] move version checker --- openlp/core/__init__.py | 20 +- openlp/core/common/versionchecker.py | 171 ++++++++++++++++++ openlp/core/lib/plugin.py | 3 +- openlp/core/ui/aboutform.py | 2 +- openlp/core/ui/exceptionform.py | 7 +- openlp/core/ui/mainwindow.py | 2 +- openlp/core/utils/__init__.py | 152 ---------------- openlp/plugins/songs/lib/openlyricsxml.py | 2 +- .../openlp_core_common/test_versionchecker.py | 63 +++++++ .../functional/openlp_core_utils/test_init.py | 22 +-- .../openlp_core_utils/test_utils.py | 1 - 11 files changed, 253 insertions(+), 192 deletions(-) create mode 100644 openlp/core/common/versionchecker.py create mode 100644 tests/functional/openlp_core_common/test_versionchecker.py diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index b663fc55b..db135ef10 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -27,26 +27,26 @@ All the core functions of the OpenLP application including the GUI, settings, logging and a plugin framework are contained within the openlp.core module. """ -import os -import sys -import logging import argparse -from traceback import format_exception +import logging +import os import shutil +import sys import time +from traceback import format_exception + from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \ check_directory_exists, is_macosx, is_win, translate +from openlp.core.common.versionchecker import VersionThread, get_application_version from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources -from openlp.core.ui.mainwindow import MainWindow -from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm -from openlp.core.ui.firsttimeform import FirstTimeForm -from openlp.core.ui.exceptionform import ExceptionForm from openlp.core.ui import SplashScreen -from openlp.core.utils import VersionThread, get_application_version - +from openlp.core.ui.exceptionform import ExceptionForm +from openlp.core.ui.firsttimeform import FirstTimeForm +from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm +from openlp.core.ui.mainwindow import MainWindow __all__ = ['OpenLP', 'main'] diff --git a/openlp/core/common/versionchecker.py b/openlp/core/common/versionchecker.py new file mode 100644 index 000000000..5dcab6a6f --- /dev/null +++ b/openlp/core/common/versionchecker.py @@ -0,0 +1,171 @@ +import logging +import os +import platform +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime +from distutils.version import LooseVersion +from subprocess import Popen, PIPE + +from openlp.core.common import AppLocation, Settings + +from PyQt5 import QtCore + +log = logging.getLogger(__name__) + +APPLICATION_VERSION = {} +CONNECTION_TIMEOUT = 30 +CONNECTION_RETRIES = 2 + + +class VersionThread(QtCore.QThread): + """ + A special Qt thread class to fetch the version of OpenLP from the website. + This is threaded so that it doesn't affect the loading time of OpenLP. + """ + def __init__(self, main_window): + """ + Constructor for the thread class. + + :param main_window: The main window Object. + """ + log.debug("VersionThread - Initialise") + super(VersionThread, self).__init__(None) + self.main_window = main_window + + def run(self): + """ + Run the thread. + """ + self.sleep(1) + log.debug('Version thread - run') + app_version = get_application_version() + version = check_latest_version(app_version) + log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full'])))) + if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])): + self.main_window.openlp_version_check.emit('%s' % version) + + +def get_application_version(): + """ + Returns the application version of the running instance of OpenLP:: + + {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'} + """ + global APPLICATION_VERSION + if APPLICATION_VERSION: + return APPLICATION_VERSION + if '--dev-version' in sys.argv or '-d' in sys.argv: + # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied + # there. + + # Get the revision of this tree. + bzr = Popen(('bzr', 'revno'), stdout=PIPE) + tree_revision, error = bzr.communicate() + tree_revision = tree_revision.decode() + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr log') + + # Get all tags. + bzr = Popen(('bzr', 'tags'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr tags') + tags = list(map(bytes.decode, output.splitlines())) + if not tags: + tag_version = '0.0.0' + tag_revision = '0' + else: + # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from + # another series. + tags = [tag for tag in tags if tag.split()[-1].strip() != '?'] + # Get the last tag and split it in a revision and tag name. + tag_version, tag_revision = tags[-1].split() + # 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.strip() + else: + 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) + file_path = os.path.join(file_path, '.version') + version_file = None + try: + version_file = open(file_path, 'r') + full_version = str(version_file.read()).rstrip() + except IOError: + log.exception('Error in version file.') + full_version = '0.0.0-bzr000' + finally: + if version_file: + version_file.close() + bits = full_version.split('-') + APPLICATION_VERSION = { + 'full': full_version, + 'version': bits[0], + 'build': bits[1] if len(bits) > 1 else None + } + if APPLICATION_VERSION['build']: + log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build']) + else: + log.info('Openlp version %s' % APPLICATION_VERSION['version']) + return APPLICATION_VERSION + + +def check_latest_version(current_version): + """ + Check the latest version of OpenLP against the version file on the OpenLP + site. + + **Rules around versions and version files:** + + * If a version number has a build (i.e. -bzr1234), then it is a nightly. + * If a version number's minor version is an odd number, it is a development release. + * If a version number's minor version is an even number, it is a stable release. + + :param current_version: The current version of OpenLP. + """ + version_string = current_version['full'] + # set to prod in the distribution config file. + settings = Settings() + settings.beginGroup('core') + last_test = settings.value('last version test') + this_test = str(datetime.now().date()) + settings.setValue('last version test', this_test) + settings.endGroup() + if last_test != this_test: + if current_version['build']: + req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt') + else: + version_parts = current_version['version'].split('.') + if int(version_parts[1]) % 2 != 0: + req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt') + else: + req = urllib.request.Request('http://www.openlp.org/files/version.txt') + req.add_header('User-Agent', 'OpenLP/%s %s/%s; ' % (current_version['full'], platform.system(), + platform.release())) + remote_version = None + retries = 0 + while True: + try: + remote_version = str(urllib.request.urlopen(req, None, + timeout=CONNECTION_TIMEOUT).read().decode()).strip() + except (urllib.error.URLError, ConnectionError): + if retries > CONNECTION_RETRIES: + log.exception('Failed to download the latest OpenLP version file') + else: + retries += 1 + time.sleep(0.1) + continue + break + if remote_version: + version_string = remote_version + return version_string + diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index 6c19ac1dd..b9c8ca5f0 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -24,11 +24,10 @@ Provide the generic plugin functionality for OpenLP plugins. """ import logging - from PyQt5 import QtCore from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings -from openlp.core.utils import get_application_version +from openlp.core.common.versionchecker import get_application_version log = logging.getLogger(__name__) diff --git a/openlp/core/ui/aboutform.py b/openlp/core/ui/aboutform.py index b376d4646..fc29f968c 100644 --- a/openlp/core/ui/aboutform.py +++ b/openlp/core/ui/aboutform.py @@ -26,8 +26,8 @@ import webbrowser from PyQt5 import QtCore, QtWidgets +from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import translate -from openlp.core.utils import get_application_version from .aboutdialog import UiAboutDialog diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 2e4661579..68dd9705f 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -23,18 +23,17 @@ The actual exception dialog form. """ import logging -import re import os import platform +import re import bs4 import sqlalchemy +from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets from lxml import etree from openlp.core.common import RegistryProperties, is_linux -from PyQt5 import Qt, QtCore, QtGui, QtWebKit, QtWidgets - try: import migrate MIGRATE_VERSION = getattr(migrate, '__version__', '< 0.7') @@ -74,7 +73,7 @@ except ImportError: VLC_VERSION = '-' from openlp.core.common import Settings, UiStrings, translate -from openlp.core.utils import get_application_version +from openlp.core.common.versionchecker import get_application_version from .exceptiondialog import Ui_ExceptionDialog diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 9618b1c48..228969ad1 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -37,6 +37,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, \ check_directory_exists, translate, is_win, is_macosx, add_actions from openlp.core.common.actions import ActionList, CategoryOrder +from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \ build_icon from openlp.core.lib.ui import UiStrings, create_action @@ -46,7 +47,6 @@ from openlp.core.ui.firsttimeform import FirstTimeForm from openlp.core.ui.media import MediaController from openlp.core.ui.printserviceform import PrintServiceForm from openlp.core.ui.projector.manager import ProjectorManager -from openlp.core.utils import get_application_version log = logging.getLogger(__name__) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 3cd1b0295..656c9f1cb 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -24,7 +24,6 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ import logging import os -import platform import re import socket import sys @@ -32,8 +31,6 @@ import time import urllib.error import urllib.parse import urllib.request -from datetime import datetime -from distutils.version import LooseVersion from http.client import HTTPException from random import randint from shutil import which @@ -90,34 +87,6 @@ CONNECTION_TIMEOUT = 30 CONNECTION_RETRIES = 2 -class VersionThread(QtCore.QThread): - """ - A special Qt thread class to fetch the version of OpenLP from the website. - This is threaded so that it doesn't affect the loading time of OpenLP. - """ - def __init__(self, main_window): - """ - Constructor for the thread class. - - :param main_window: The main window Object. - """ - log.debug("VersionThread - Initialise") - super(VersionThread, self).__init__(None) - self.main_window = main_window - - def run(self): - """ - Run the thread. - """ - self.sleep(1) - log.debug('Version thread - run') - app_version = get_application_version() - version = check_latest_version(app_version) - log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full'])))) - if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])): - self.main_window.openlp_version_check.emit('%s' % version) - - class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): """ Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 @@ -145,127 +114,6 @@ class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) -def get_application_version(): - """ - Returns the application version of the running instance of OpenLP:: - - {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'} - """ - global APPLICATION_VERSION - if APPLICATION_VERSION: - return APPLICATION_VERSION - if '--dev-version' in sys.argv or '-d' in sys.argv: - # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied - # there. - - # Get the revision of this tree. - bzr = Popen(('bzr', 'revno'), stdout=PIPE) - tree_revision, error = bzr.communicate() - tree_revision = tree_revision.decode() - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr log') - - # Get all tags. - bzr = Popen(('bzr', 'tags'), stdout=PIPE) - output, error = bzr.communicate() - code = bzr.wait() - if code != 0: - raise Exception('Error running bzr tags') - tags = list(map(bytes.decode, output.splitlines())) - if not tags: - tag_version = '0.0.0' - tag_revision = '0' - else: - # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from - # another series. - tags = [tag for tag in tags if tag.split()[-1].strip() != '?'] - # Get the last tag and split it in a revision and tag name. - tag_version, tag_revision = tags[-1].split() - # 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.strip() - else: - 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) - file_path = os.path.join(file_path, '.version') - version_file = None - try: - version_file = open(file_path, 'r') - full_version = str(version_file.read()).rstrip() - except IOError: - log.exception('Error in version file.') - full_version = '0.0.0-bzr000' - finally: - if version_file: - version_file.close() - bits = full_version.split('-') - APPLICATION_VERSION = { - 'full': full_version, - 'version': bits[0], - 'build': bits[1] if len(bits) > 1 else None - } - if APPLICATION_VERSION['build']: - log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build']) - else: - log.info('Openlp version %s' % APPLICATION_VERSION['version']) - return APPLICATION_VERSION - - -def check_latest_version(current_version): - """ - Check the latest version of OpenLP against the version file on the OpenLP - site. - - **Rules around versions and version files:** - - * If a version number has a build (i.e. -bzr1234), then it is a nightly. - * If a version number's minor version is an odd number, it is a development release. - * If a version number's minor version is an even number, it is a stable release. - - :param current_version: The current version of OpenLP. - """ - version_string = current_version['full'] - # set to prod in the distribution config file. - settings = Settings() - settings.beginGroup('core') - last_test = settings.value('last version test') - this_test = str(datetime.now().date()) - settings.setValue('last version test', this_test) - settings.endGroup() - if last_test != this_test: - if current_version['build']: - req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt') - else: - version_parts = current_version['version'].split('.') - if int(version_parts[1]) % 2 != 0: - req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt') - else: - req = urllib.request.Request('http://www.openlp.org/files/version.txt') - req.add_header('User-Agent', 'OpenLP/%s %s/%s; ' % (current_version['full'], platform.system(), - platform.release())) - remote_version = None - retries = 0 - while True: - try: - remote_version = str(urllib.request.urlopen(req, None, - timeout=CONNECTION_TIMEOUT).read().decode()).strip() - except (urllib.error.URLError, ConnectionError): - if retries > CONNECTION_RETRIES: - log.exception('Failed to download the latest OpenLP version file') - else: - retries += 1 - time.sleep(0.1) - continue - break - if remote_version: - version_string = remote_version - return version_string - - def get_filesystem_encoding(): """ Returns the name of the encoding used to convert Unicode filenames into system file names. diff --git a/openlp/plugins/songs/lib/openlyricsxml.py b/openlp/plugins/songs/lib/openlyricsxml.py index bba60baa2..806df438d 100644 --- a/openlp/plugins/songs/lib/openlyricsxml.py +++ b/openlp/plugins/songs/lib/openlyricsxml.py @@ -62,10 +62,10 @@ import re from lxml import etree, objectify from openlp.core.common import translate +from openlp.core.common.versionchecker import get_application_version from openlp.core.lib import FormattingTags from openlp.plugins.songs.lib import VerseType, clean_song from openlp.plugins.songs.lib.db import Author, AuthorType, Book, Song, Topic -from openlp.core.utils import get_application_version log = logging.getLogger(__name__) diff --git a/tests/functional/openlp_core_common/test_versionchecker.py b/tests/functional/openlp_core_common/test_versionchecker.py new file mode 100644 index 000000000..ab2269667 --- /dev/null +++ b/tests/functional/openlp_core_common/test_versionchecker.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.common.versionchecker package. +""" +from unittest import TestCase + +from openlp.core.common.settings import Settings +from openlp.core.common.versionchecker import VersionThread +from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin + + +class TestVersionchecker(TestMixin, TestCase): + + def setUp(self): + """ + Create an instance and a few example actions. + """ + self.build_settings() + + def tearDown(self): + """ + Clean up + """ + self.destroy_settings() + + def version_thread_triggered_test(self): + """ + Test the version thread call does not trigger UI + :return: + """ + # GIVEN: a equal version setup and the data is not today. + mocked_main_window = MagicMock() + Settings().setValue('core/last version test', '1950-04-01') + # WHEN: We check to see if the version is different . + with patch('PyQt5.QtCore.QThread'),\ + patch('openlp.core.common.versionchecker.get_application_version') as mocked_get_application_version: + mocked_get_application_version.return_value = {'version': '1.0.0', 'build': '', 'full': '2.0.4'} + version_thread = VersionThread(mocked_main_window) + version_thread.run() + # THEN: If the version has changed the main window is notified + self.assertTrue(mocked_main_window.openlp_version_check.emit.called, + 'The main windows should have been notified') \ No newline at end of file diff --git a/tests/functional/openlp_core_utils/test_init.py b/tests/functional/openlp_core_utils/test_init.py index 6a62f3a7f..d01c9f300 100644 --- a/tests/functional/openlp_core_utils/test_init.py +++ b/tests/functional/openlp_core_utils/test_init.py @@ -24,9 +24,8 @@ Package to test the openlp.core.utils.actions package. """ from unittest import TestCase -from openlp.core.common.settings import Settings -from openlp.core.utils import VersionThread, get_uno_command -from tests.functional import MagicMock, patch +from openlp.core.utils import get_uno_command +from tests.functional import patch from tests.helpers.testmixin import TestMixin @@ -44,23 +43,6 @@ class TestInitFunctions(TestMixin, TestCase): """ self.destroy_settings() - def version_thread_triggered_test(self): - """ - Test the version thread call does not trigger UI - :return: - """ - # GIVEN: a equal version setup and the data is not today. - mocked_main_window = MagicMock() - Settings().setValue('core/last version test', '1950-04-01') - # WHEN: We check to see if the version is different . - with patch('PyQt5.QtCore.QThread'),\ - patch('openlp.core.utils.get_application_version') as mocked_get_application_version: - mocked_get_application_version.return_value = {'version': '1.0.0', 'build': '', 'full': '2.0.4'} - version_thread = VersionThread(mocked_main_window) - version_thread.run() - # THEN: If the version has changed the main window is notified - self.assertTrue(mocked_main_window.openlp_version_check.emit.called, - 'The main windows should have been notified') def get_uno_command_libreoffice_command_exists_test(self): """ diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 0c4234a3d..28d890a22 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -27,7 +27,6 @@ from unittest import TestCase from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ split_filename, _get_user_agent, get_web_page, get_uno_instance -from openlp.core.common.languagemanager import get_locale_key, get_natural_key from tests.functional import MagicMock, patch From 98a021b89fca9ac81c34bfd4b233076a16fa64f2 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 21:03:19 +0100 Subject: [PATCH 32/55] Pep8 --- openlp/core/utils/__init__.py | 5 ++--- tests/functional/openlp_core_common/test_versionchecker.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 656c9f1cb..2713973f2 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -34,11 +34,10 @@ import urllib.request from http.client import HTTPException from random import randint from shutil import which -from subprocess import Popen, PIPE -from PyQt5 import QtGui, QtCore +from PyQt5 import QtGui -from openlp.core.common import Registry, AppLocation, Settings, is_win, is_macosx +from openlp.core.common import Registry, is_win, is_macosx if not is_win() and not is_macosx(): try: diff --git a/tests/functional/openlp_core_common/test_versionchecker.py b/tests/functional/openlp_core_common/test_versionchecker.py index ab2269667..d14bfa679 100644 --- a/tests/functional/openlp_core_common/test_versionchecker.py +++ b/tests/functional/openlp_core_common/test_versionchecker.py @@ -60,4 +60,4 @@ class TestVersionchecker(TestMixin, TestCase): version_thread.run() # THEN: If the version has changed the main window is notified self.assertTrue(mocked_main_window.openlp_version_check.emit.called, - 'The main windows should have been notified') \ No newline at end of file + 'The main windows should have been notified') From 85587ce2f3a1ad908988fc810ab71ee99bbb4936 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 21:14:04 +0100 Subject: [PATCH 33/55] uno commands --- openlp/core/common/__init__.py | 32 +++++++++++++++++ openlp/core/utils/__init__.py | 34 +------------------ .../presentations/lib/impresscontroller.py | 2 +- .../plugins/songs/lib/importers/openoffice.py | 2 +- .../functional/openlp_core_utils/test_init.py | 2 +- .../openlp_core_utils/test_utils.py | 3 +- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index b382a719f..b951ff038 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -30,6 +30,7 @@ import re import sys import traceback from ipaddress import IPv4Address, IPv6Address, AddressValueError +from shutil import which from PyQt5 import QtCore from PyQt5.QtCore import QCryptographicHash as QHash @@ -257,3 +258,34 @@ def add_actions(target, actions): target.addSeparator() else: target.addAction(action) + + +def get_uno_command(connection_type='pipe'): + """ + Returns the UNO command to launch an libreoffice.org instance. + """ + for command in ['libreoffice', 'soffice']: + if which(command): + break + else: + raise FileNotFoundError('Command not found') + + OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard' + if connection_type == 'pipe': + CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"' + else: + CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"' + return '%s %s %s' % (command, OPTIONS, CONNECTION) + + +def get_uno_instance(resolver, connection_type='pipe'): + """ + Returns a running libreoffice.org instance. + + :param resolver: The UNO resolver to use to find a running instance. + """ + log.debug('get UNO Desktop Openoffice - resolve') + if connection_type == 'pipe': + return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + else: + return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') \ No newline at end of file diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 2713973f2..81610c1ab 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -33,7 +33,6 @@ import urllib.parse import urllib.request from http.client import HTTPException from random import randint -from shutil import which from PyQt5 import QtGui @@ -279,36 +278,5 @@ def get_web_page(url, header=None, update_openlp=False): return page -def get_uno_command(connection_type='pipe'): - """ - Returns the UNO command to launch an libreoffice.org instance. - """ - for command in ['libreoffice', 'soffice']: - if which(command): - break - else: - raise FileNotFoundError('Command not found') - - OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard' - if connection_type == 'pipe': - CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"' - else: - CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"' - return '%s %s %s' % (command, OPTIONS, CONNECTION) - - -def get_uno_instance(resolver, connection_type='pipe'): - """ - Returns a running libreoffice.org instance. - - :param resolver: The UNO resolver to use to find a running instance. - """ - log.debug('get UNO Desktop Openoffice - resolve') - if connection_type == 'pipe': - return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') - else: - return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') - __all__ = ['get_application_version', 'check_latest_version', - 'get_filesystem_encoding', 'get_web_page', 'get_uno_command', 'get_uno_instance', - 'delete_file', 'clean_filename'] + 'get_filesystem_encoding', 'get_web_page', 'delete_file', 'clean_filename'] diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 183a05ac5..25844f8b3 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -35,7 +35,7 @@ import logging import os import time -from openlp.core.common import is_win, Registry +from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance if is_win(): from win32com.client import Dispatch diff --git a/openlp/plugins/songs/lib/importers/openoffice.py b/openlp/plugins/songs/lib/importers/openoffice.py index 5f94d6e77..e8bd60439 100644 --- a/openlp/plugins/songs/lib/importers/openoffice.py +++ b/openlp/plugins/songs/lib/importers/openoffice.py @@ -25,7 +25,7 @@ import time from PyQt5 import QtCore -from openlp.core.common import is_win +from openlp.core.common import is_win, get_uno_command, get_uno_instance from openlp.core.utils import get_uno_command, get_uno_instance from openlp.core.lib import translate from .songimport import SongImport diff --git a/tests/functional/openlp_core_utils/test_init.py b/tests/functional/openlp_core_utils/test_init.py index d01c9f300..c2ff662b3 100644 --- a/tests/functional/openlp_core_utils/test_init.py +++ b/tests/functional/openlp_core_utils/test_init.py @@ -24,7 +24,7 @@ Package to test the openlp.core.utils.actions package. """ from unittest import TestCase -from openlp.core.utils import get_uno_command +from openlp.core.common import get_uno_command from tests.functional import patch from tests.helpers.testmixin import TestMixin diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 28d890a22..05941431f 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -26,7 +26,8 @@ import os from unittest import TestCase from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ - split_filename, _get_user_agent, get_web_page, get_uno_instance + split_filename, _get_user_agent, get_web_page +from openlp.core.common import get_uno_instance from tests.functional import MagicMock, patch From 5dd4b8e3861de669ca989d7d3383f183b65cfbaa Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 21:27:33 +0100 Subject: [PATCH 34/55] Pep8 --- openlp/core/common/__init__.py | 2 +- .../openlp_core_common/test_init.py | 28 ++++++++++++++++- .../openlp_core_utils/test_utils.py | 31 ++----------------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index b951ff038..4dc8d595a 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -288,4 +288,4 @@ def get_uno_instance(resolver, connection_type='pipe'): if connection_type == 'pipe': return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') else: - return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') \ No newline at end of file + return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 1df0bcedc..2a8a3215d 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -24,7 +24,7 @@ Functional tests to test the AppLocation class and related methods. """ from unittest import TestCase -from openlp.core.common import add_actions +from openlp.core.common import add_actions, get_uno_instance from tests.functional import MagicMock @@ -92,3 +92,29 @@ class TestInit(TestCase): # THEN: The addSeparator method is called, and the addAction method is called mocked_target.addSeparator.assert_called_with() mocked_target.addAction.assert_called_with('action') + + def get_uno_instance_pipe_test(self): + """ + Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI + """ + # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe" + mock_resolver = MagicMock() + + # WHEN: get_uno_instance() is called + get_uno_instance(mock_resolver) + + # THEN: the resolve method is called with the correct argument + mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') + + def get_uno_instance_socket_test(self): + """ + Test that when the UNO connection type is other than "pipe" the resolver is given the "socket" URI + """ + # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "socket" + mock_resolver = MagicMock() + + # WHEN: get_uno_instance() is called + get_uno_instance(mock_resolver, 'socket') + + # THEN: the resolve method is called with the correct argument + mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 05941431f..0886a3997 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -27,7 +27,6 @@ from unittest import TestCase from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ split_filename, _get_user_agent, get_web_page -from openlp.core.common import get_uno_instance from tests.functional import MagicMock, patch @@ -180,32 +179,6 @@ class TestUtils(TestCase): self.assertEqual(mocked_log.exception.call_count, 1) self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError') - def get_uno_instance_pipe_test(self): - """ - Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI - """ - # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe" - mock_resolver = MagicMock() - - # WHEN: get_uno_instance() is called - get_uno_instance(mock_resolver) - - # THEN: the resolve method is called with the correct argument - mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') - - def get_uno_instance_socket_test(self): - """ - Test that when the UNO connection type is other than "pipe" the resolver is given the "socket" URI - """ - # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "socket" - mock_resolver = MagicMock() - - # WHEN: get_uno_instance() is called - get_uno_instance(mock_resolver, 'socket') - - # THEN: the resolve method is called with the correct argument - mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') - def get_user_agent_linux_test(self): """ Test that getting a user agent on Linux returns a user agent suitable for Linux @@ -287,7 +260,7 @@ class TestUtils(TestCase): with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.utils.Registry') as MockRegistry: + patch('openlp.core.common.Registry') as MockRegistry: # GIVEN: Mocked out objects and a fake URL mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object @@ -374,7 +347,7 @@ class TestUtils(TestCase): with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.utils.Registry') as MockRegistry: + patch('openlp.core.common.Registry') as MockRegistry: # GIVEN: Mocked out objects, a fake URL mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object From 8a1b62fdcd25219803f7f6a21df70dc890316ce8 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 21:41:08 +0100 Subject: [PATCH 35/55] move tests --- .../openlp_core_common/test_init.py | 81 ++++++++++++- .../functional/openlp_core_utils/test_init.py | 111 ------------------ 2 files changed, 79 insertions(+), 113 deletions(-) delete mode 100644 tests/functional/openlp_core_utils/test_init.py diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 2a8a3215d..6754874b0 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -24,8 +24,8 @@ Functional tests to test the AppLocation class and related methods. """ from unittest import TestCase -from openlp.core.common import add_actions, get_uno_instance -from tests.functional import MagicMock +from openlp.core.common import add_actions, get_uno_instance, get_uno_command, get_frozen_path +from tests.functional import MagicMock, patch class TestInit(TestCase): @@ -33,6 +33,18 @@ class TestInit(TestCase): A test suite to test out various methods around the common __init__ class. """ + def setUp(self): + """ + Create an instance and a few example actions. + """ + self.build_settings() + + def tearDown(self): + """ + Clean up + """ + self.destroy_settings() + def add_actions_empty_list_test(self): """ Test that no actions are added when the list is empty @@ -118,3 +130,68 @@ class TestInit(TestCase): # THEN: the resolve method is called with the correct argument mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') + + def get_uno_command_libreoffice_command_exists_test(self): + """ + Test the ``get_uno_command`` function uses the libreoffice command when available. + :return: + """ + + # GIVEN: A patched 'which' method which returns a path when called with 'libreoffice' + with patch('openlp.core.utils.which', + **{'side_effect': lambda command: {'libreoffice': '/usr/bin/libreoffice'}[command]}): + # WHEN: Calling get_uno_command + result = get_uno_command() + + # THEN: The command 'libreoffice' should be called with the appropriate parameters + self.assertEquals(result, + 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' + ' "--accept=pipe,name=openlp_pipe;urp;"') + + def get_uno_command_only_soffice_command_exists_test(self): + """ + Test the ``get_uno_command`` function uses the soffice command when the libreoffice command is not available. + :return: + """ + + # GIVEN: A patched 'which' method which returns None when called with 'libreoffice' and a path when called with + # 'soffice' + with patch('openlp.core.utils.which', + **{'side_effect': lambda command: {'libreoffice': None, 'soffice': '/usr/bin/soffice'}[ + command]}): + # WHEN: Calling get_uno_command + result = get_uno_command() + + # THEN: The command 'soffice' should be called with the appropriate parameters + self.assertEquals(result, 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard' + ' "--accept=pipe,name=openlp_pipe;urp;"') + + def get_uno_command_when_no_command_exists_test(self): + """ + Test the ``get_uno_command`` function raises an FileNotFoundError when neither the libreoffice or soffice + commands are available. + :return: + """ + + # GIVEN: A patched 'which' method which returns None + with patch('openlp.core.utils.which', **{'return_value': None}): + # WHEN: Calling get_uno_command + + # THEN: a FileNotFoundError exception should be raised + self.assertRaises(FileNotFoundError, get_uno_command) + + def get_uno_command_connection_type_test(self): + """ + Test the ``get_uno_command`` function when the connection type is anything other than pipe. + :return: + """ + + # GIVEN: A patched 'which' method which returns 'libreoffice' + with patch('openlp.core.utils.which', **{'return_value': 'libreoffice'}): + # WHEN: Calling get_uno_command with a connection type other than pipe + result = get_uno_command('socket') + + # THEN: The connection parameters should be set for socket + self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' + ' "--accept=socket,host=localhost,port=2002;urp;"') + diff --git a/tests/functional/openlp_core_utils/test_init.py b/tests/functional/openlp_core_utils/test_init.py deleted file mode 100644 index c2ff662b3..000000000 --- a/tests/functional/openlp_core_utils/test_init.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Package to test the openlp.core.utils.actions package. -""" -from unittest import TestCase - -from openlp.core.common import get_uno_command -from tests.functional import patch -from tests.helpers.testmixin import TestMixin - - -class TestInitFunctions(TestMixin, TestCase): - - def setUp(self): - """ - Create an instance and a few example actions. - """ - self.build_settings() - - def tearDown(self): - """ - Clean up - """ - self.destroy_settings() - - - def get_uno_command_libreoffice_command_exists_test(self): - """ - Test the ``get_uno_command`` function uses the libreoffice command when available. - :return: - """ - - # GIVEN: A patched 'which' method which returns a path when called with 'libreoffice' - with patch('openlp.core.utils.which', - **{'side_effect': lambda command: {'libreoffice': '/usr/bin/libreoffice'}[command]}): - - # WHEN: Calling get_uno_command - result = get_uno_command() - - # THEN: The command 'libreoffice' should be called with the appropriate parameters - self.assertEquals(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=pipe,name=openlp_pipe;urp;"') - - def get_uno_command_only_soffice_command_exists_test(self): - """ - Test the ``get_uno_command`` function uses the soffice command when the libreoffice command is not available. - :return: - """ - - # GIVEN: A patched 'which' method which returns None when called with 'libreoffice' and a path when called with - # 'soffice' - with patch('openlp.core.utils.which', - **{'side_effect': lambda command: {'libreoffice': None, 'soffice': '/usr/bin/soffice'}[command]}): - - # WHEN: Calling get_uno_command - result = get_uno_command() - - # THEN: The command 'soffice' should be called with the appropriate parameters - self.assertEquals(result, 'soffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=pipe,name=openlp_pipe;urp;"') - - def get_uno_command_when_no_command_exists_test(self): - """ - Test the ``get_uno_command`` function raises an FileNotFoundError when neither the libreoffice or soffice - commands are available. - :return: - """ - - # GIVEN: A patched 'which' method which returns None - with patch('openlp.core.utils.which', **{'return_value': None}): - - # WHEN: Calling get_uno_command - - # THEN: a FileNotFoundError exception should be raised - self.assertRaises(FileNotFoundError, get_uno_command) - - def get_uno_command_connection_type_test(self): - """ - Test the ``get_uno_command`` function when the connection type is anything other than pipe. - :return: - """ - - # GIVEN: A patched 'which' method which returns 'libreoffice' - with patch('openlp.core.utils.which', **{'return_value': 'libreoffice'}): - - # WHEN: Calling get_uno_command with a connection type other than pipe - result = get_uno_command('socket') - - # THEN: The connection parameters should be set for socket - self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' - ' "--accept=socket,host=localhost,port=2002;urp;"') From 0b480f5e8dd2e0411deee9bb79d7700dc8cf5fde Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 21:47:33 +0100 Subject: [PATCH 36/55] add test mixin --- tests/functional/openlp_core_common/test_init.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 6754874b0..573ba6632 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -24,11 +24,12 @@ Functional tests to test the AppLocation class and related methods. """ from unittest import TestCase -from openlp.core.common import add_actions, get_uno_instance, get_uno_command, get_frozen_path +from openlp.core.common import add_actions, get_uno_instance, get_uno_command from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin -class TestInit(TestCase): +class TestInit(TestCase, TestMixin): """ A test suite to test out various methods around the common __init__ class. """ From ee62d9fa6c70248bdd685ddd44b7ea2e4454cfa9 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 22:01:24 +0100 Subject: [PATCH 37/55] change package --- tests/functional/openlp_core_common/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 573ba6632..dd6616ccb 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -139,7 +139,7 @@ class TestInit(TestCase, TestMixin): """ # GIVEN: A patched 'which' method which returns a path when called with 'libreoffice' - with patch('openlp.core.utils.which', + with patch('openlp.core.common.which', **{'side_effect': lambda command: {'libreoffice': '/usr/bin/libreoffice'}[command]}): # WHEN: Calling get_uno_command result = get_uno_command() @@ -157,7 +157,7 @@ class TestInit(TestCase, TestMixin): # GIVEN: A patched 'which' method which returns None when called with 'libreoffice' and a path when called with # 'soffice' - with patch('openlp.core.utils.which', + with patch('openlp.core.common.which', **{'side_effect': lambda command: {'libreoffice': None, 'soffice': '/usr/bin/soffice'}[ command]}): # WHEN: Calling get_uno_command @@ -175,7 +175,7 @@ class TestInit(TestCase, TestMixin): """ # GIVEN: A patched 'which' method which returns None - with patch('openlp.core.utils.which', **{'return_value': None}): + with patch('openlp.core.common.which', **{'return_value': None}): # WHEN: Calling get_uno_command # THEN: a FileNotFoundError exception should be raised @@ -188,7 +188,7 @@ class TestInit(TestCase, TestMixin): """ # GIVEN: A patched 'which' method which returns 'libreoffice' - with patch('openlp.core.utils.which', **{'return_value': 'libreoffice'}): + with patch('openlp.core.common.which', **{'return_value': 'libreoffice'}): # WHEN: Calling get_uno_command with a connection type other than pipe result = get_uno_command('socket') From d440891819db2a6c1ba47a231364b4aa33e86c6b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 22:11:23 +0100 Subject: [PATCH 38/55] fix import --- tests/functional/openlp_core_utils/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 0886a3997..7caaaf199 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -347,7 +347,7 @@ class TestUtils(TestCase): with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.common.Registry') as MockRegistry: + patch('openlp.core.utils.Registry') as MockRegistry: # GIVEN: Mocked out objects, a fake URL mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object From 3e8af699c3c3956ecf38f1fa753a3fb51836f106 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 4 Apr 2016 22:19:37 +0100 Subject: [PATCH 39/55] fix import issues --- openlp/plugins/presentations/lib/pdfcontroller.py | 2 +- openlp/plugins/presentations/lib/pptviewcontroller.py | 2 +- tests/functional/openlp_plugins/remotes/test_remotetab.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index 64e197eba..dbea84327 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -27,7 +27,7 @@ import re from shutil import which from subprocess import check_output, CalledProcessError, STDOUT -from openlp.core.utils import AppLocation +from openlp.core.common import AppLocation from openlp.core.common import Settings, is_win, trace_error_handler from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index aba0aa88e..5989e775a 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -34,7 +34,7 @@ if is_win(): from ctypes import cdll from ctypes.wintypes import RECT -from openlp.core.utils import AppLocation +from openlp.core.common import AppLocation from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py index d541dc6e3..24375740f 100644 --- a/tests/functional/openlp_plugins/remotes/test_remotetab.py +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -99,7 +99,7 @@ class TestRemoteTab(TestCase, TestMixin): """ # GIVEN: A mocked location with patch('openlp.core.common.Settings') as mocked_class, \ - patch('openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch('openlp.core.common.applocation.AppLocation.get_directory') as mocked_get_directory, \ patch('openlp.core.common.check_directory_exists') as mocked_check_directory_exists, \ patch('openlp.core.common.applocation.os') as mocked_os: # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() @@ -127,7 +127,7 @@ class TestRemoteTab(TestCase, TestMixin): """ # GIVEN: A mocked location with patch('openlp.core.common.Settings') as mocked_class, \ - patch('openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch('openlp.core.common.applocation.AppLocation.get_directory') as mocked_get_directory, \ patch('openlp.core.common.check_directory_exists') as mocked_check_directory_exists, \ patch('openlp.core.common.applocation.os') as mocked_os: # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() From cfc3a978a82aa1cb2c23dc53bfa66fd171e4bd33 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 17:34:21 +0100 Subject: [PATCH 40/55] fix import issues --- openlp/plugins/presentations/lib/impresscontroller.py | 3 ++- openlp/plugins/presentations/lib/pptviewcontroller.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 25844f8b3..af4eed071 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -57,7 +57,8 @@ else: from PyQt5 import QtCore from openlp.core.lib import ScreenList -from openlp.core.utils import delete_file, get_uno_command, get_uno_instance +from openlp.core.utils import delete_file, \ +from openlp.core.common import get_uno_command, get_uno_instance from .presentationcontroller import PresentationController, PresentationDocument, TextType diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 5989e775a..c5e1b351f 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -20,7 +20,6 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import logging import os import logging import zipfile From 6dcd4b54d93cd494757c176326d076f3cd1ced9f Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 17:43:55 +0100 Subject: [PATCH 41/55] fix import issues --- openlp/plugins/presentations/lib/impresscontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index af4eed071..8f5742712 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -57,7 +57,7 @@ else: from PyQt5 import QtCore from openlp.core.lib import ScreenList -from openlp.core.utils import delete_file, \ +from openlp.core.utils import delete_file from openlp.core.common import get_uno_command, get_uno_instance from .presentationcontroller import PresentationController, PresentationDocument, TextType From f19280c88d1ef0ea232b4ac137b87fd26eccd8cf Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 17:58:29 +0100 Subject: [PATCH 42/55] move method --- openlp/core/common/__init__.py | 10 ++++++++++ openlp/core/ui/thememanager.py | 4 ++-- openlp/core/utils/__init__.py | 12 +----------- tests/functional/openlp_core_utils/test_utils.py | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 4dc8d595a..a20aa48ae 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -289,3 +289,13 @@ def get_uno_instance(resolver, connection_type='pipe'): return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext') else: return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext') + + +def get_filesystem_encoding(): + """ + Returns the name of the encoding used to convert Unicode filenames into system file names. + """ + encoding = sys.getfilesystemencoding() + if encoding is None: + encoding = sys.getdefaultencoding() + return encoding \ No newline at end of file diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index fc632d61d..9d1b2d8e7 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -30,13 +30,13 @@ from xml.etree.ElementTree import ElementTree, XML from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ - check_directory_exists, UiStrings, translate, is_win + check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.utils import delete_file, get_filesystem_encoding +from openlp.core.utils import delete_file from openlp.core.common.languagemanager import get_locale_key diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 81610c1ab..2fea5df80 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -112,16 +112,6 @@ class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) -def get_filesystem_encoding(): - """ - Returns the name of the encoding used to convert Unicode filenames into system file names. - """ - encoding = sys.getfilesystemencoding() - if encoding is None: - encoding = sys.getdefaultencoding() - return encoding - - def get_images_filter(): """ Returns a filter string for a file dialog containing all the supported image formats. @@ -279,4 +269,4 @@ def get_web_page(url, header=None, update_openlp=False): __all__ = ['get_application_version', 'check_latest_version', - 'get_filesystem_encoding', 'get_web_page', 'delete_file', 'clean_filename'] + 'get_web_page', 'delete_file', 'clean_filename'] diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 7caaaf199..0bd4f1645 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -25,8 +25,8 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, \ - split_filename, _get_user_agent, get_web_page +from openlp.core.utils import clean_filename, delete_file, split_filename, _get_user_agent, get_web_page +from openlp.core.common import get_filesystem_encoding from tests.functional import MagicMock, patch From b7da0be71ec99b2bc44b60a5d9371a4853a67a08 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 18:10:51 +0100 Subject: [PATCH 43/55] move method --- openlp/core/common/__init__.py | 30 ++++++++++++++++++- openlp/core/lib/db.py | 3 +- openlp/core/ui/servicemanager.py | 3 +- openlp/core/ui/thememanager.py | 3 +- openlp/core/utils/__init__.py | 30 +------------------ .../plugins/bibles/forms/bibleupgradeform.py | 4 +-- openlp/plugins/bibles/lib/manager.py | 3 +- openlp/plugins/images/lib/mediaitem.py | 5 ++-- .../presentations/lib/impresscontroller.py | 3 +- .../plugins/songs/lib/importers/openoffice.py | 1 - .../openlp_core_utils/test_utils.py | 4 +-- 11 files changed, 42 insertions(+), 47 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index a20aa48ae..7b5c9f3fd 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -298,4 +298,32 @@ def get_filesystem_encoding(): encoding = sys.getfilesystemencoding() if encoding is None: encoding = sys.getdefaultencoding() - return encoding \ No newline at end of file + return encoding + + +def split_filename(path): + """ + Return a list of the parts in a given path. + """ + path = os.path.abspath(path) + if not os.path.isfile(path): + return path, '' + else: + return os.path.split(path) + + +def delete_file(file_path_name): + """ + Deletes a file from the system. + + :param file_path_name: The file, including path, to delete. + """ + if not file_path_name: + return False + try: + if os.path.exists(file_path_name): + os.remove(file_path_name) + return True + except (IOError, OSError): + log.exception("Unable to delete file %s" % file_path_name) + return False \ No newline at end of file diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py index d63dee23c..7ae3cdc6f 100644 --- a/openlp/core/lib/db.py +++ b/openlp/core/lib/db.py @@ -34,9 +34,8 @@ from sqlalchemy.pool import NullPool from alembic.migration import MigrationContext from alembic.operations import Operations -from openlp.core.common import AppLocation, Settings, translate +from openlp.core.common import AppLocation, Settings, translate, delete_file from openlp.core.lib.ui import critical_error_message_box -from openlp.core.utils import delete_file log = logging.getLogger(__name__) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 129d24ca1..66cbdf1b7 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -33,12 +33,11 @@ from tempfile import mkstemp from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, ThemeLevel, OpenLPMixin, \ - RegistryMixin, check_directory_exists, UiStrings, translate + RegistryMixin, check_directory_exists, UiStrings, translate, split_filename, delete_file from openlp.core.common.actions import ActionList, CategoryOrder from openlp.core.lib import OpenLPToolbar, ServiceItem, ItemCapabilities, PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box, create_widget_action, find_and_set_in_combo_box from openlp.core.ui import ServiceNoteForm, ServiceItemEditForm, StartTimeForm -from openlp.core.utils import delete_file, split_filename from openlp.core.common.languagemanager import format_time diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index 9d1b2d8e7..a80640150 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -30,13 +30,12 @@ from xml.etree.ElementTree import ElementTree, XML from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \ - check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding + check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file from openlp.core.lib import FileDialog, ImageSource, OpenLPToolbar, ValidationError, get_text_file_string, build_icon, \ check_item_selected, create_thumb, validate_thumb from openlp.core.lib.theme import ThemeXML, BackgroundType from openlp.core.lib.ui import critical_error_message_box, create_widget_action from openlp.core.ui import FileRenameForm, ThemeForm -from openlp.core.utils import delete_file from openlp.core.common.languagemanager import get_locale_key diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 2fea5df80..0be2338b3 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -142,17 +142,6 @@ def is_not_image_file(file_name): return True -def split_filename(path): - """ - Return a list of the parts in a given path. - """ - path = os.path.abspath(path) - if not os.path.isfile(path): - return path, '' - else: - return os.path.split(path) - - def clean_filename(filename): """ Removes invalid characters from the given ``filename``. @@ -164,23 +153,6 @@ def clean_filename(filename): return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) -def delete_file(file_path_name): - """ - Deletes a file from the system. - - :param file_path_name: The file, including path, to delete. - """ - if not file_path_name: - return False - try: - if os.path.exists(file_path_name): - os.remove(file_path_name) - return True - except (IOError, OSError): - log.exception("Unable to delete file %s" % file_path_name) - return False - - def _get_user_agent(): """ Return a user agent customised for the platform the user is on. @@ -269,4 +241,4 @@ def get_web_page(url, header=None, update_openlp=False): __all__ = ['get_application_version', 'check_latest_version', - 'get_web_page', 'delete_file', 'clean_filename'] + 'get_web_page', 'clean_filename'] diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index 4adfeaf3b..611e6ead3 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -29,10 +29,10 @@ from tempfile import gettempdir from PyQt5 import QtCore, QtWidgets -from openlp.core.common import Registry, AppLocation, UiStrings, Settings, check_directory_exists, translate +from openlp.core.common import Registry, AppLocation, UiStrings, Settings, check_directory_exists, translate, \ + delete_file from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings -from openlp.core.utils import delete_file from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta, OldBibleDB, BiblesResourcesDB from openlp.plugins.bibles.lib.http import BSExtract, BGExtract, CWExtract diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 8cecbe0af..b8b7ee56f 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -23,8 +23,7 @@ import logging import os -from openlp.core.common import RegistryProperties, AppLocation, Settings, translate -from openlp.core.utils import delete_file +from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, delete_file from openlp.plugins.bibles.lib import parse_reference, get_reference_separator, LanguageSelection from openlp.plugins.bibles.lib.db import BibleDB, BibleMeta from .csvbible import CSVBible diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index c57442156..6efcc32df 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -25,11 +25,12 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate +from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \ + delete_file from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.utils import delete_file, get_images_filter +from openlp.core.utils import get_images_filter from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 8f5742712..29af3a375 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -35,7 +35,7 @@ import logging import os import time -from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance +from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file if is_win(): from win32com.client import Dispatch @@ -57,7 +57,6 @@ else: from PyQt5 import QtCore from openlp.core.lib import ScreenList -from openlp.core.utils import delete_file from openlp.core.common import get_uno_command, get_uno_instance from .presentationcontroller import PresentationController, PresentationDocument, TextType diff --git a/openlp/plugins/songs/lib/importers/openoffice.py b/openlp/plugins/songs/lib/importers/openoffice.py index e8bd60439..b21f3a7d3 100644 --- a/openlp/plugins/songs/lib/importers/openoffice.py +++ b/openlp/plugins/songs/lib/importers/openoffice.py @@ -26,7 +26,6 @@ import time from PyQt5 import QtCore from openlp.core.common import is_win, get_uno_command, get_uno_instance -from openlp.core.utils import get_uno_command, get_uno_instance from openlp.core.lib import translate from .songimport import SongImport diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 0bd4f1645..d0bb07f95 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -25,8 +25,8 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import clean_filename, delete_file, split_filename, _get_user_agent, get_web_page -from openlp.core.common import get_filesystem_encoding +from openlp.core.utils import clean_filename, _get_user_agent, get_web_page +from openlp.core.common import get_filesystem_encoding, split_filename, delete_file from tests.functional import MagicMock, patch From 3e8e72be851082a7d48b678ab3c7c3ea8f2641f1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 18:30:20 +0100 Subject: [PATCH 44/55] move methods and clean up --- openlp/core/common/__init__.py | 48 +++++++++++++++- openlp/core/common/languagemanager.py | 1 + openlp/core/ui/advancedtab.py | 3 +- openlp/core/ui/themeform.py | 3 +- openlp/core/utils/__init__.py | 55 +----------------- .../plugins/bibles/forms/bibleimportform.py | 2 +- openlp/plugins/bibles/lib/db.py | 3 +- openlp/plugins/images/lib/mediaitem.py | 3 +- openlp/plugins/songs/lib/__init__.py | 3 +- openlp/plugins/songs/lib/openlyricsexport.py | 3 +- .../openlp_core_utils/test_first_time.py | 57 ------------------- .../openlp_core_utils/test_utils.py | 4 +- .../openlp_core_utils/test_utils.py | 2 +- 13 files changed, 58 insertions(+), 129 deletions(-) delete mode 100644 tests/functional/openlp_core_utils/test_first_time.py diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 7b5c9f3fd..ba10da87c 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -32,7 +32,7 @@ import traceback from ipaddress import IPv4Address, IPv6Address, AddressValueError from shutil import which -from PyQt5 import QtCore +from PyQt5 import QtCore, QtGui from PyQt5.QtCore import QCryptographicHash as QHash log = logging.getLogger(__name__ + '.__init__') @@ -40,6 +40,9 @@ log = logging.getLogger(__name__ + '.__init__') FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)') SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])') +CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE) +INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE) +IMAGES_FILTER = None def trace_error_handler(logger): @@ -326,4 +329,45 @@ def delete_file(file_path_name): return True except (IOError, OSError): log.exception("Unable to delete file %s" % file_path_name) - return False \ No newline at end of file + return False + + +def get_images_filter(): + """ + Returns a filter string for a file dialog containing all the supported image formats. + """ + global IMAGES_FILTER + if not IMAGES_FILTER: + log.debug('Generating images filter.') + formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats())))) + visible_formats = '(*.%s)' % '; *.'.join(formats) + actual_formats = '(*.%s)' % ' *.'.join(formats) + IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats) + return IMAGES_FILTER + + +def is_not_image_file(file_name): + """ + Validate that the file is not an image file. + + :param file_name: File name to be checked. + """ + if not file_name: + return True + else: + formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()] + file_part, file_extension = os.path.splitext(str(file_name)) + if file_extension[1:].lower() in formats and os.path.exists(file_name): + return False + return True + + +def clean_filename(filename): + """ + Removes invalid characters from the given ``filename``. + + :param filename: The "dirty" file name to clean. + """ + if not isinstance(filename, str): + filename = str(filename, 'utf-8') + return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) \ No newline at end of file diff --git a/openlp/core/common/languagemanager.py b/openlp/core/common/languagemanager.py index c3e736d65..52e9e9f13 100644 --- a/openlp/core/common/languagemanager.py +++ b/openlp/core/common/languagemanager.py @@ -33,6 +33,7 @@ from openlp.core.common import AppLocation, Settings, translate, is_win, is_maco log = logging.getLogger(__name__) +ICU_COLLATOR = None DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index ce26ae808..f32672f58 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -29,10 +29,9 @@ import sys from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate +from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate, get_images_filter from openlp.core.lib import ColorButton, SettingsTab, build_icon from openlp.core.common.languagemanager import format_time -from openlp.core.utils import get_images_filter log = logging.getLogger(__name__) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index cd81ec800..d620a0f79 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -27,11 +27,10 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import Registry, RegistryProperties, UiStrings, translate +from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file from openlp.core.lib.theme import BackgroundType, BackgroundGradientType from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import ThemeLayoutForm -from openlp.core.utils import get_images_filter, is_not_image_file from .themewizard import Ui_ThemeWizard log = logging.getLogger(__name__) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 0be2338b3..76cfad0e8 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -23,8 +23,6 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ import logging -import os -import re import socket import sys import time @@ -34,8 +32,6 @@ import urllib.request from http.client import HTTPException from random import randint -from PyQt5 import QtGui - from openlp.core.common import Registry, is_win, is_macosx if not is_win() and not is_macosx(): @@ -46,16 +42,8 @@ if not is_win() and not is_macosx(): BaseDirectory = None XDG_BASE_AVAILABLE = False -from openlp.core.common import translate - log = logging.getLogger(__name__ + '.__init__') -APPLICATION_VERSION = {} -IMAGES_FILTER = None -ICU_COLLATOR = None -CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE) -INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE) -DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE) USER_AGENTS = { 'win32': [ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', @@ -112,47 +100,6 @@ class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) -def get_images_filter(): - """ - Returns a filter string for a file dialog containing all the supported image formats. - """ - global IMAGES_FILTER - if not IMAGES_FILTER: - log.debug('Generating images filter.') - formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats())))) - visible_formats = '(*.%s)' % '; *.'.join(formats) - actual_formats = '(*.%s)' % ' *.'.join(formats) - IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats) - return IMAGES_FILTER - - -def is_not_image_file(file_name): - """ - Validate that the file is not an image file. - - :param file_name: File name to be checked. - """ - if not file_name: - return True - else: - formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()] - file_part, file_extension = os.path.splitext(str(file_name)) - if file_extension[1:].lower() in formats and os.path.exists(file_name): - return False - return True - - -def clean_filename(filename): - """ - Removes invalid characters from the given ``filename``. - - :param filename: The "dirty" file name to clean. - """ - if not isinstance(filename, str): - filename = str(filename, 'utf-8') - return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) - - def _get_user_agent(): """ Return a user agent customised for the platform the user is on. @@ -241,4 +188,4 @@ def get_web_page(url, header=None, update_openlp=False): __all__ = ['get_application_version', 'check_latest_version', - 'get_web_page', 'clean_filename'] + 'get_web_page'] diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 676801eb8..27dbea963 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -28,7 +28,7 @@ import urllib.error from PyQt5 import QtWidgets -from openlp.core.common import AppLocation, Settings, UiStrings, translate +from openlp.core.common import AppLocation, Settings, UiStrings, translate, clean_filename from openlp.core.lib.db import delete_database from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.wizard import OpenLPWizard, WizardStrings diff --git a/openlp/plugins/bibles/lib/db.py b/openlp/plugins/bibles/lib/db.py index b77117e21..8dcf8c042 100644 --- a/openlp/plugins/bibles/lib/db.py +++ b/openlp/plugins/bibles/lib/db.py @@ -33,10 +33,9 @@ from sqlalchemy.exc import OperationalError from sqlalchemy.orm import class_mapper, mapper, relation from sqlalchemy.orm.exc import UnmappedClassError -from openlp.core.common import Registry, RegistryProperties, AppLocation, translate +from openlp.core.common import Registry, RegistryProperties, AppLocation, translate, clean_filename from openlp.core.lib.db import BaseModel, init_db, Manager from openlp.core.lib.ui import critical_error_message_box -from openlp.core.utils import clean_filename from openlp.plugins.bibles.lib import upgrade log = logging.getLogger(__name__) diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 6efcc32df..f35fd48c7 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -26,11 +26,10 @@ import os from PyQt5 import QtCore, QtGui, QtWidgets from openlp.core.common import Registry, AppLocation, Settings, UiStrings, check_directory_exists, translate, \ - delete_file + delete_file, get_images_filter from openlp.core.lib import ItemCapabilities, MediaManagerItem, ServiceItemContext, StringContent, TreeWidgetWithDnD,\ build_icon, check_item_selected, create_thumb, validate_thumb from openlp.core.lib.ui import create_widget_action, critical_error_message_box -from openlp.core.utils import get_images_filter from openlp.core.common.languagemanager import get_locale_key from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py index ce80c4b1e..1d45f52b2 100644 --- a/openlp/plugins/songs/lib/__init__.py +++ b/openlp/plugins/songs/lib/__init__.py @@ -29,9 +29,8 @@ import re from PyQt5 import QtWidgets -from openlp.core.common import AppLocation +from openlp.core.common import AppLocation, CONTROL_CHARS from openlp.core.lib import translate -from openlp.core.utils import CONTROL_CHARS from openlp.plugins.songs.lib.db import MediaFile, Song from .db import Author from .ui import SongStrings diff --git a/openlp/plugins/songs/lib/openlyricsexport.py b/openlp/plugins/songs/lib/openlyricsexport.py index a8cffb418..d5ca31e18 100644 --- a/openlp/plugins/songs/lib/openlyricsexport.py +++ b/openlp/plugins/songs/lib/openlyricsexport.py @@ -28,8 +28,7 @@ import os from lxml import etree -from openlp.core.common import RegistryProperties, check_directory_exists, translate -from openlp.core.utils import clean_filename +from openlp.core.common import RegistryProperties, check_directory_exists, translate, clean_filename from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics log = logging.getLogger(__name__) diff --git a/tests/functional/openlp_core_utils/test_first_time.py b/tests/functional/openlp_core_utils/test_first_time.py deleted file mode 100644 index b9a7622a2..000000000 --- a/tests/functional/openlp_core_utils/test_first_time.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -Package to test the openlp.core.utils.__init__ package. -""" - -from unittest import TestCase -import urllib.request -import urllib.error -import urllib.parse - -from tests.functional import MagicMock, patch -from tests.helpers.testmixin import TestMixin - -from openlp.core.utils import CONNECTION_TIMEOUT, CONNECTION_RETRIES, get_web_page - - -class TestFirstTimeWizard(TestMixin, TestCase): - """ - Test First Time Wizard import functions - """ - def webpage_connection_retry_test(self): - """ - Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031 - """ - # GIVEN: Initial settings and mocks - with patch.object(urllib.request, 'urlopen') as mocked_urlopen: - mocked_urlopen.side_effect = ConnectionError - - # WHEN: A webpage is requested - try: - get_web_page(url='http://localhost') - except: - pass - - # THEN: urlopen should have been called CONNECTION_RETRIES + 1 count - self.assertEquals(mocked_urlopen.call_count, CONNECTION_RETRIES + 1, - 'get_web_page() should have tried {} times'.format(CONNECTION_RETRIES)) diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index d0bb07f95..614be7bb9 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -25,8 +25,8 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import clean_filename, _get_user_agent, get_web_page -from openlp.core.common import get_filesystem_encoding, split_filename, delete_file +from openlp.core.utils import _get_user_agent, get_web_page +from openlp.core.common import get_filesystem_encoding, split_filename, delete_file, clean_filename from tests.functional import MagicMock, patch diff --git a/tests/interfaces/openlp_core_utils/test_utils.py b/tests/interfaces/openlp_core_utils/test_utils.py index 1124abbb7..2c0d9a572 100644 --- a/tests/interfaces/openlp_core_utils/test_utils.py +++ b/tests/interfaces/openlp_core_utils/test_utils.py @@ -25,7 +25,7 @@ Functional tests to test the AppLocation class and related methods. import os from unittest import TestCase -from openlp.core.utils import is_not_image_file +from openlp.core.common import is_not_image_file from tests.utils.constants import TEST_RESOURCES_PATH from tests.helpers.testmixin import TestMixin From 2df1169ea8ce2deae0991a00f41cba74be6e0fe1 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 19:11:42 +0100 Subject: [PATCH 45/55] fix tests --- openlp/core/utils/__init__.py | 10 +- .../openlp_core_common/test_init.py | 147 +++++++++++++++++- .../openlp_core_utils/test_utils.py | 146 ----------------- 3 files changed, 147 insertions(+), 156 deletions(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 76cfad0e8..653717c93 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -32,15 +32,7 @@ import urllib.request from http.client import HTTPException from random import randint -from openlp.core.common import Registry, is_win, is_macosx - -if not is_win() and not is_macosx(): - try: - from xdg import BaseDirectory - XDG_BASE_AVAILABLE = True - except ImportError: - BaseDirectory = None - XDG_BASE_AVAILABLE = False +from openlp.core.common import Registry log = logging.getLogger(__name__ + '.__init__') diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index dd6616ccb..452a75a95 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -22,9 +22,11 @@ """ Functional tests to test the AppLocation class and related methods. """ +import os from unittest import TestCase -from openlp.core.common import add_actions, get_uno_instance, get_uno_command +from openlp.core.common import add_actions, get_uno_instance, get_uno_command, delete_file, get_filesystem_encoding, \ + split_filename, clean_filename from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -196,3 +198,146 @@ class TestInit(TestCase, TestMixin): self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' ' "--accept=socket,host=localhost,port=2002;urp;"') + + def get_filesystem_encoding_sys_function_not_called_test(self): + """ + Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function + """ + # GIVEN: sys.getfilesystemencoding returns "cp1252" + with patch('openlp.core.common.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \ + patch('openlp.core.common.sys.getdefaultencoding') as mocked_getdefaultencoding: + mocked_getfilesystemencoding.return_value = 'cp1252' + + # WHEN: get_filesystem_encoding() is called + result = get_filesystem_encoding() + + # THEN: getdefaultencoding should have been called + mocked_getfilesystemencoding.assert_called_with() + self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called') + self.assertEqual('cp1252', result, 'The result should be "cp1252"') + + def get_filesystem_encoding_sys_function_is_called_test(self): + """ + Test the get_filesystem_encoding() function calls the sys.getdefaultencoding() function + """ + # GIVEN: sys.getfilesystemencoding returns None and sys.getdefaultencoding returns "utf-8" + with patch('openlp.core.common.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \ + patch('openlp.core.common.sys.getdefaultencoding') as mocked_getdefaultencoding: + mocked_getfilesystemencoding.return_value = None + mocked_getdefaultencoding.return_value = 'utf-8' + + # WHEN: get_filesystem_encoding() is called + result = get_filesystem_encoding() + + # THEN: getdefaultencoding should have been called + mocked_getfilesystemencoding.assert_called_with() + mocked_getdefaultencoding.assert_called_with() + self.assertEqual('utf-8', result, 'The result should be "utf-8"') + + def split_filename_with_file_path_test(self): + """ + Test the split_filename() function with a path to a file + """ + # GIVEN: A path to a file. + if os.name == 'nt': + file_path = 'C:\\home\\user\\myfile.txt' + wanted_result = ('C:\\home\\user', 'myfile.txt') + else: + file_path = '/home/user/myfile.txt' + wanted_result = ('/home/user', 'myfile.txt') + with patch('openlp.core.common.os.path.isfile') as mocked_is_file: + mocked_is_file.return_value = True + + # WHEN: Split the file name. + result = split_filename(file_path) + + # THEN: A tuple should be returned. + self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned') + + def split_filename_with_dir_path_test(self): + """ + Test the split_filename() function with a path to a directory + """ + # GIVEN: A path to a dir. + if os.name == 'nt': + file_path = 'C:\\home\\user\\mydir' + wanted_result = ('C:\\home\\user\\mydir', '') + else: + file_path = '/home/user/mydir' + wanted_result = ('/home/user/mydir', '') + with patch('openlp.core.common.os.path.isfile') as mocked_is_file: + mocked_is_file.return_value = False + + # WHEN: Split the file name. + result = split_filename(file_path) + + # THEN: A tuple should be returned. + self.assertEqual(wanted_result, result, + 'A two-entry tuple with the directory and file name (empty) should have been returned.') + + def clean_filename_test(self): + """ + Test the clean_filename() function + """ + # GIVEN: A invalid file name and the valid file name. + invalid_name = 'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py' + wanted_name = 'A_file_with_invalid_characters______________________.py' + + # WHEN: Clean the name. + result = clean_filename(invalid_name) + + # THEN: The file name should be cleaned. + self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.') + + def delete_file_no_path_test(self): + """ + Test the delete_file function when called with out a valid path + """ + # GIVEN: A blank path + # WEHN: Calling delete_file + result = delete_file('') + + # THEN: delete_file should return False + self.assertFalse(result, "delete_file should return False when called with ''") + + def delete_file_path_success_test(self): + """ + Test the delete_file function when it successfully deletes a file + """ + # GIVEN: A mocked os which returns True when os.path.exists is called + with patch('openlp.core.common.os', **{'path.exists.return_value': False}): + + # WHEN: Calling delete_file with a file path + result = delete_file('path/file.ext') + + # THEN: delete_file should return True + self.assertTrue(result, 'delete_file should return True when it successfully deletes a file') + + def delete_file_path_no_file_exists_test(self): + """ + Test the delete_file function when the file to remove does not exist + """ + # GIVEN: A mocked os which returns False when os.path.exists is called + with patch('openlp.core.common.os', **{'path.exists.return_value': False}): + + # WHEN: Calling delete_file with a file path + result = delete_file('path/file.ext') + + # THEN: delete_file should return True + self.assertTrue(result, 'delete_file should return True when the file doesnt exist') + + def delete_file_path_exception_test(self): + """ + Test the delete_file function when os.remove raises an exception + """ + # GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is + # called. + with patch('openlp.core.common.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \ + patch('openlp.core.common.log') as mocked_log: + + # WHEN: Calling delete_file with a file path + result = delete_file('path/file.ext') + + # THEN: delete_file should log and exception and return False + self.assertEqual(mocked_log.exception.call_count, 1) + self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError') diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 614be7bb9..4035fceef 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -22,11 +22,9 @@ """ Functional tests to test the AppLocation class and related methods. """ -import os from unittest import TestCase from openlp.core.utils import _get_user_agent, get_web_page -from openlp.core.common import get_filesystem_encoding, split_filename, delete_file, clean_filename from tests.functional import MagicMock, patch @@ -35,150 +33,6 @@ class TestUtils(TestCase): """ A test suite to test out various methods around the AppLocation class. """ - - def get_filesystem_encoding_sys_function_not_called_test(self): - """ - Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function - """ - # GIVEN: sys.getfilesystemencoding returns "cp1252" - with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \ - patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding: - mocked_getfilesystemencoding.return_value = 'cp1252' - - # WHEN: get_filesystem_encoding() is called - result = get_filesystem_encoding() - - # THEN: getdefaultencoding should have been called - mocked_getfilesystemencoding.assert_called_with() - self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called') - self.assertEqual('cp1252', result, 'The result should be "cp1252"') - - def get_filesystem_encoding_sys_function_is_called_test(self): - """ - Test the get_filesystem_encoding() function calls the sys.getdefaultencoding() function - """ - # GIVEN: sys.getfilesystemencoding returns None and sys.getdefaultencoding returns "utf-8" - with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \ - patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding: - mocked_getfilesystemencoding.return_value = None - mocked_getdefaultencoding.return_value = 'utf-8' - - # WHEN: get_filesystem_encoding() is called - result = get_filesystem_encoding() - - # THEN: getdefaultencoding should have been called - mocked_getfilesystemencoding.assert_called_with() - mocked_getdefaultencoding.assert_called_with() - self.assertEqual('utf-8', result, 'The result should be "utf-8"') - - def split_filename_with_file_path_test(self): - """ - Test the split_filename() function with a path to a file - """ - # GIVEN: A path to a file. - if os.name == 'nt': - file_path = 'C:\\home\\user\\myfile.txt' - wanted_result = ('C:\\home\\user', 'myfile.txt') - else: - file_path = '/home/user/myfile.txt' - wanted_result = ('/home/user', 'myfile.txt') - with patch('openlp.core.utils.os.path.isfile') as mocked_is_file: - mocked_is_file.return_value = True - - # WHEN: Split the file name. - result = split_filename(file_path) - - # THEN: A tuple should be returned. - self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned') - - def split_filename_with_dir_path_test(self): - """ - Test the split_filename() function with a path to a directory - """ - # GIVEN: A path to a dir. - if os.name == 'nt': - file_path = 'C:\\home\\user\\mydir' - wanted_result = ('C:\\home\\user\\mydir', '') - else: - file_path = '/home/user/mydir' - wanted_result = ('/home/user/mydir', '') - with patch('openlp.core.utils.os.path.isfile') as mocked_is_file: - mocked_is_file.return_value = False - - # WHEN: Split the file name. - result = split_filename(file_path) - - # THEN: A tuple should be returned. - self.assertEqual(wanted_result, result, - 'A two-entry tuple with the directory and file name (empty) should have been returned.') - - def clean_filename_test(self): - """ - Test the clean_filename() function - """ - # GIVEN: A invalid file name and the valid file name. - invalid_name = 'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py' - wanted_name = 'A_file_with_invalid_characters______________________.py' - - # WHEN: Clean the name. - result = clean_filename(invalid_name) - - # THEN: The file name should be cleaned. - self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.') - - def delete_file_no_path_test(self): - """ - Test the delete_file function when called with out a valid path - """ - # GIVEN: A blank path - # WEHN: Calling delete_file - result = delete_file('') - - # THEN: delete_file should return False - self.assertFalse(result, "delete_file should return False when called with ''") - - def delete_file_path_success_test(self): - """ - Test the delete_file function when it successfully deletes a file - """ - # GIVEN: A mocked os which returns True when os.path.exists is called - with patch('openlp.core.utils.os', **{'path.exists.return_value': False}): - - # WHEN: Calling delete_file with a file path - result = delete_file('path/file.ext') - - # THEN: delete_file should return True - self.assertTrue(result, 'delete_file should return True when it successfully deletes a file') - - def delete_file_path_no_file_exists_test(self): - """ - Test the delete_file function when the file to remove does not exist - """ - # GIVEN: A mocked os which returns False when os.path.exists is called - with patch('openlp.core.utils.os', **{'path.exists.return_value': False}): - - # WHEN: Calling delete_file with a file path - result = delete_file('path/file.ext') - - # THEN: delete_file should return True - self.assertTrue(result, 'delete_file should return True when the file doesnt exist') - - def delete_file_path_exception_test(self): - """ - Test the delete_file function when os.remove raises an exception - """ - # GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is - # called. - with patch('openlp.core.utils.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \ - patch('openlp.core.utils.log') as mocked_log: - - # WHEN: Calling delete_file with a file path - result = delete_file('path/file.ext') - - # THEN: delete_file should log and exception and return False - self.assertEqual(mocked_log.exception.call_count, 1) - self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError') - def get_user_agent_linux_test(self): """ Test that getting a user agent on Linux returns a user agent suitable for Linux From 6729ea9d19192200dabf92eaf8917d8586cd9a7a Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 19:33:50 +0100 Subject: [PATCH 46/55] move file --- .../__init__.py => lib/webpagereader.py} | 0 openlp/core/ui/firsttimeform.py | 2 +- setup.py | 4 +- .../openlp_core_ui/test_first_time.py | 57 +++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) rename openlp/core/{utils/__init__.py => lib/webpagereader.py} (100%) create mode 100644 tests/functional/openlp_core_ui/test_first_time.py diff --git a/openlp/core/utils/__init__.py b/openlp/core/lib/webpagereader.py similarity index 100% rename from openlp/core/utils/__init__.py rename to openlp/core/lib/webpagereader.py diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 0d7c129bf..f2be3b29c 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -39,7 +39,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin translate, clean_button_text, trace_error_handler from openlp.core.lib import PluginStatus, build_icon from openlp.core.lib.ui import critical_error_message_box -from openlp.core.utils import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT +from openlp.core.lib.webpagereader import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT from .firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index ba7d13e27..78f2692b1 100755 --- a/setup.py +++ b/setup.py @@ -65,8 +65,8 @@ def natural_sort(seq): return temp -# NOTE: The following code is a duplicate of the code in openlp/core/utils/__init__.py. Any fix applied here should also -# be applied there. +# NOTE: The following code is a duplicate of the code in openlp/core/common/checkversion.py. +# Any fix applied here should also be applied there. ver_file = None try: # Get the revision of this tree. diff --git a/tests/functional/openlp_core_ui/test_first_time.py b/tests/functional/openlp_core_ui/test_first_time.py new file mode 100644 index 000000000..b9a7622a2 --- /dev/null +++ b/tests/functional/openlp_core_ui/test_first_time.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.utils.__init__ package. +""" + +from unittest import TestCase +import urllib.request +import urllib.error +import urllib.parse + +from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin + +from openlp.core.utils import CONNECTION_TIMEOUT, CONNECTION_RETRIES, get_web_page + + +class TestFirstTimeWizard(TestMixin, TestCase): + """ + Test First Time Wizard import functions + """ + def webpage_connection_retry_test(self): + """ + Test get_web_page will attempt CONNECTION_RETRIES+1 connections - bug 1409031 + """ + # GIVEN: Initial settings and mocks + with patch.object(urllib.request, 'urlopen') as mocked_urlopen: + mocked_urlopen.side_effect = ConnectionError + + # WHEN: A webpage is requested + try: + get_web_page(url='http://localhost') + except: + pass + + # THEN: urlopen should have been called CONNECTION_RETRIES + 1 count + self.assertEquals(mocked_urlopen.call_count, CONNECTION_RETRIES + 1, + 'get_web_page() should have tried {} times'.format(CONNECTION_RETRIES)) From 7c88006c7bac216eb496d1ada133861f4089590c Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 19:44:50 +0100 Subject: [PATCH 48/55] fix tests --- openlp/plugins/bibles/lib/http.py | 2 +- tests/functional/openlp_core_common/test_applocation.py | 2 +- tests/functional/openlp_core_ui/test_first_time.py | 4 ++-- tests/functional/openlp_core_utils/test_utils.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 35b1f4bcb..c81e65575 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -32,7 +32,7 @@ from bs4 import BeautifulSoup, NavigableString, Tag from openlp.core.common import Registry, RegistryProperties, translate from openlp.core.lib.ui import critical_error_message_box -from openlp.core.utils import get_web_page +from openlp.core.lib.webpagereader import get_web_page from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book diff --git a/tests/functional/openlp_core_common/test_applocation.py b/tests/functional/openlp_core_common/test_applocation.py index 6bb218046..0d42867e2 100644 --- a/tests/functional/openlp_core_common/test_applocation.py +++ b/tests/functional/openlp_core_common/test_applocation.py @@ -171,7 +171,7 @@ class TestAppLocation(TestCase): """ Test the _get_frozen_path() function when the application is not frozen (compiled by PyInstaller) """ - with patch('openlp.core.utils.sys') as mocked_sys: + with patch('openlp.core.common.sys') as mocked_sys: # GIVEN: The sys module "without" a "frozen" attribute mocked_sys.frozen = None diff --git a/tests/functional/openlp_core_ui/test_first_time.py b/tests/functional/openlp_core_ui/test_first_time.py index b9a7622a2..43c8c0bc1 100644 --- a/tests/functional/openlp_core_ui/test_first_time.py +++ b/tests/functional/openlp_core_ui/test_first_time.py @@ -28,10 +28,10 @@ import urllib.request import urllib.error import urllib.parse -from tests.functional import MagicMock, patch +from tests.functional import patch from tests.helpers.testmixin import TestMixin -from openlp.core.utils import CONNECTION_TIMEOUT, CONNECTION_RETRIES, get_web_page +from openlp.core.lib.webpagereader import CONNECTION_RETRIES, get_web_page class TestFirstTimeWizard(TestMixin, TestCase): diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 4035fceef..c5c14378a 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -24,7 +24,7 @@ Functional tests to test the AppLocation class and related methods. """ from unittest import TestCase -from openlp.core.utils import _get_user_agent, get_web_page +from openlp.core.lib.webpagereader import _get_user_agent, get_web_page from tests.functional import MagicMock, patch From fb2de75cbcb2129608f21703649fc04619a2ed24 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 20:11:10 +0100 Subject: [PATCH 49/55] fix tests --- .../test_webpagereader.py} | 34 +++++++++---------- .../functional/openlp_core_utils/__init__.py | 21 ------------ .../test_utils.py | 0 .../interfaces/openlp_core_utils/__init__.py | 21 ------------ 4 files changed, 17 insertions(+), 59 deletions(-) rename tests/functional/{openlp_core_utils/test_utils.py => openlp_core_lib/test_webpagereader.py} (88%) delete mode 100644 tests/functional/openlp_core_utils/__init__.py rename tests/interfaces/{openlp_core_utils => openlp_core_common}/test_utils.py (100%) delete mode 100644 tests/interfaces/openlp_core_utils/__init__.py diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_lib/test_webpagereader.py similarity index 88% rename from tests/functional/openlp_core_utils/test_utils.py rename to tests/functional/openlp_core_lib/test_webpagereader.py index c5c14378a..772c8c562 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_lib/test_webpagereader.py @@ -37,7 +37,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on Linux returns a user agent suitable for Linux """ - with patch('openlp.core.utils.sys') as mocked_sys: + with patch('openlp.core.lib.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'linux2' @@ -53,7 +53,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on Windows returns a user agent suitable for Windows """ - with patch('openlp.core.utils.sys') as mocked_sys: + with patch('openlp.core.lib.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'win32' @@ -68,7 +68,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on OS X returns a user agent suitable for OS X """ - with patch('openlp.core.utils.sys') as mocked_sys: + with patch('openlp.core.lib.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'darwin' @@ -83,7 +83,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent """ - with patch('openlp.core.utils.sys') as mocked_sys: + with patch('openlp.core.lib.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'freebsd' @@ -111,9 +111,9 @@ class TestUtils(TestCase): """ Test that the get_web_page method works correctly """ - with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ + with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ patch('openlp.core.common.Registry') as MockRegistry: # GIVEN: Mocked out objects and a fake URL mocked_request_object = MagicMock() @@ -141,9 +141,9 @@ class TestUtils(TestCase): """ Test that adding a header to the call to get_web_page() adds the header to the request """ - with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.utils._get_user_agent') as mock_get_user_agent: + with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: # GIVEN: Mocked out objects, a fake URL and a fake header mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object @@ -170,9 +170,9 @@ class TestUtils(TestCase): """ Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request """ - with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.utils._get_user_agent') as mock_get_user_agent: + with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: # GIVEN: Mocked out objects, a fake URL and a fake header mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object @@ -198,10 +198,10 @@ class TestUtils(TestCase): """ Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() """ - with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \ - patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \ - patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.utils.Registry') as MockRegistry: + with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ + patch('openlp.core.lib.Registry') as MockRegistry: # GIVEN: Mocked out objects, a fake URL mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object diff --git a/tests/functional/openlp_core_utils/__init__.py b/tests/functional/openlp_core_utils/__init__.py deleted file mode 100644 index 02bded5b0..000000000 --- a/tests/functional/openlp_core_utils/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### diff --git a/tests/interfaces/openlp_core_utils/test_utils.py b/tests/interfaces/openlp_core_common/test_utils.py similarity index 100% rename from tests/interfaces/openlp_core_utils/test_utils.py rename to tests/interfaces/openlp_core_common/test_utils.py diff --git a/tests/interfaces/openlp_core_utils/__init__.py b/tests/interfaces/openlp_core_utils/__init__.py deleted file mode 100644 index 02bded5b0..000000000 --- a/tests/interfaces/openlp_core_utils/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2016 OpenLP Developers # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### From b6c49450d4cd09778daeaf402163ff9def505692 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 20:33:37 +0100 Subject: [PATCH 50/55] fix tests --- tests/functional/openlp_core_lib/test_webpagereader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_lib/test_webpagereader.py index 772c8c562..12d9bd683 100644 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ b/tests/functional/openlp_core_lib/test_webpagereader.py @@ -37,7 +37,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on Linux returns a user agent suitable for Linux """ - with patch('openlp.core.lib.sys') as mocked_sys: + with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'linux2' @@ -53,7 +53,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on Windows returns a user agent suitable for Windows """ - with patch('openlp.core.lib.sys') as mocked_sys: + with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'win32' From 3500c73494c5fe5f53188b88a86dcf0a32b7943c Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 20:44:00 +0100 Subject: [PATCH 51/55] fix tests --- tests/functional/openlp_core_lib/test_webpagereader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_lib/test_webpagereader.py index 12d9bd683..c59912b9f 100644 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ b/tests/functional/openlp_core_lib/test_webpagereader.py @@ -68,7 +68,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on OS X returns a user agent suitable for OS X """ - with patch('openlp.core.lib.sys') as mocked_sys: + with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'darwin' @@ -83,7 +83,7 @@ class TestUtils(TestCase): """ Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent """ - with patch('openlp.core.lib.sys') as mocked_sys: + with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: # GIVEN: The system is Linux mocked_sys.platform = 'freebsd' @@ -111,8 +111,8 @@ class TestUtils(TestCase): """ Test that the get_web_page method works correctly """ - with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + with patch('openlp.core.lib.webpagereader..urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ patch('openlp.core.common.Registry') as MockRegistry: # GIVEN: Mocked out objects and a fake URL From 1082254f02c073ada9d68b20fd5ab3e5e151764e Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 20:51:46 +0100 Subject: [PATCH 52/55] fix tests --- tests/functional/openlp_core_lib/test_webpagereader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_lib/test_webpagereader.py index c59912b9f..224911ab0 100644 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ b/tests/functional/openlp_core_lib/test_webpagereader.py @@ -111,7 +111,7 @@ class TestUtils(TestCase): """ Test that the get_web_page method works correctly """ - with patch('openlp.core.lib.webpagereader..urllib.request.Request') as MockRequest, \ + with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ patch('openlp.core.common.Registry') as MockRegistry: @@ -198,8 +198,8 @@ class TestUtils(TestCase): """ Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() """ - with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ patch('openlp.core.lib.Registry') as MockRegistry: # GIVEN: Mocked out objects, a fake URL From 59e3603a3e93ab814fa28217cb8ee05d30c84ad0 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 20:58:40 +0100 Subject: [PATCH 53/55] fix tests --- tests/functional/openlp_core_lib/test_webpagereader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_lib/test_webpagereader.py index 224911ab0..80b2c1f9a 100644 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ b/tests/functional/openlp_core_lib/test_webpagereader.py @@ -141,8 +141,8 @@ class TestUtils(TestCase): """ Test that adding a header to the call to get_web_page() adds the header to the request """ - with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: # GIVEN: Mocked out objects, a fake URL and a fake header mocked_request_object = MagicMock() @@ -170,8 +170,8 @@ class TestUtils(TestCase): """ Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request """ - with patch('openlp.core.lib.urllib.request.Request') as MockRequest, \ - patch('openlp.core.lib.urllib.request.urlopen') as mock_urlopen, \ + with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ + patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent: # GIVEN: Mocked out objects, a fake URL and a fake header mocked_request_object = MagicMock() @@ -201,7 +201,7 @@ class TestUtils(TestCase): with patch('openlp.core.lib.webpagereader.urllib.request.Request') as MockRequest, \ patch('openlp.core.lib.webpagereader.urllib.request.urlopen') as mock_urlopen, \ patch('openlp.core.lib.webpagereader._get_user_agent') as mock_get_user_agent, \ - patch('openlp.core.lib.Registry') as MockRegistry: + patch('openlp.core.lib.webpagereader.Registry') as MockRegistry: # GIVEN: Mocked out objects, a fake URL mocked_request_object = MagicMock() MockRequest.return_value = mocked_request_object From 31a2b37a88c13c2c3aad551f2c9da3a7c6976cf4 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 21:07:57 +0100 Subject: [PATCH 54/55] pep8 --- openlp/core/common/__init__.py | 2 +- openlp/core/common/versionchecker.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index ba10da87c..b8a1a4d2e 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -370,4 +370,4 @@ def clean_filename(filename): """ if not isinstance(filename, str): filename = str(filename, 'utf-8') - return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) \ No newline at end of file + return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename)) diff --git a/openlp/core/common/versionchecker.py b/openlp/core/common/versionchecker.py index 5dcab6a6f..136405607 100644 --- a/openlp/core/common/versionchecker.py +++ b/openlp/core/common/versionchecker.py @@ -168,4 +168,3 @@ def check_latest_version(current_version): if remote_version: version_string = remote_version return version_string - From 7f1f8cf7803cc8400cda65ccb7285db4d2b43eea Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 5 Apr 2016 21:14:50 +0100 Subject: [PATCH 55/55] pep8 --- tests/functional/openlp_core_common/test_init.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/openlp_core_common/test_init.py b/tests/functional/openlp_core_common/test_init.py index 452a75a95..2032e883e 100644 --- a/tests/functional/openlp_core_common/test_init.py +++ b/tests/functional/openlp_core_common/test_init.py @@ -198,7 +198,6 @@ class TestInit(TestCase, TestMixin): self.assertEqual(result, 'libreoffice --nologo --norestore --minimized --nodefault --nofirststartwizard' ' "--accept=socket,host=localhost,port=2002;urp;"') - def get_filesystem_encoding_sys_function_not_called_test(self): """ Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function