From 7acfe1dbfd63ac6b83f115530f44a9dbb923491b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sun, 23 Oct 2016 20:28:30 +0200 Subject: [PATCH 01/40] Add new branding to OpenLP for when we get to 2.6 --- openlp/core/ui/aboutdialog.py | 1 + openlp/core/ui/generaltab.py | 2 +- openlp/core/ui/projector/editform.py | 2 +- openlp/core/ui/projector/sourceselectform.py | 4 +- resources/images/openlp-about-logo.png | Bin 28293 -> 22584 bytes resources/images/openlp-logo.svg | 445 ++++-------------- resources/images/openlp-splash-screen.png | Bin 57089 -> 48734 bytes .../openlp_core_ui/test_settingsform.py | 12 +- 8 files changed, 109 insertions(+), 357 deletions(-) diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py index 918e48e64..3f9034dcb 100644 --- a/openlp/core/ui/aboutdialog.py +++ b/openlp/core/ui/aboutdialog.py @@ -41,6 +41,7 @@ class UiAboutDialog(object): about_dialog.setObjectName('about_dialog') about_dialog.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) self.about_dialog_layout = QtWidgets.QVBoxLayout(about_dialog) + self.about_dialog_layout.setContentsMargins(8, 8, 8, 8) self.about_dialog_layout.setObjectName('about_dialog_layout') self.logo_label = QtWidgets.QLabel(about_dialog) self.logo_label.setPixmap(QtGui.QPixmap(':/graphics/openlp-about-logo.png')) diff --git a/openlp/core/ui/generaltab.py b/openlp/core/ui/generaltab.py index 463aad73f..629e55e0f 100644 --- a/openlp/core/ui/generaltab.py +++ b/openlp/core/ui/generaltab.py @@ -44,7 +44,7 @@ class GeneralTab(SettingsTab): self.logo_file = ':/graphics/openlp-splash-screen.png' self.logo_background_color = '#ffffff' self.screens = ScreenList() - self.icon_path = ':/icon/openlp-logo-16x16.png' + self.icon_path = ':/icon/openlp-logo.svg' general_translated = translate('OpenLP.GeneralTab', 'General') super(GeneralTab, self).__init__(parent, 'Core', general_translated) diff --git a/openlp/core/ui/projector/editform.py b/openlp/core/ui/projector/editform.py index f4cf8a774..4020b2330 100644 --- a/openlp/core/ui/projector/editform.py +++ b/openlp/core/ui/projector/editform.py @@ -47,7 +47,7 @@ class Ui_ProjectorEditForm(object): Create the interface layout. """ edit_projector_dialog.setObjectName('edit_projector_dialog') - edit_projector_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo-32x32.png')) + edit_projector_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) edit_projector_dialog.setMinimumWidth(400) edit_projector_dialog.setModal(True) # Define the basic layout diff --git a/openlp/core/ui/projector/sourceselectform.py b/openlp/core/ui/projector/sourceselectform.py index c2b1c2a1b..62b0a2158 100644 --- a/openlp/core/ui/projector/sourceselectform.py +++ b/openlp/core/ui/projector/sourceselectform.py @@ -243,7 +243,7 @@ class SourceSelectTabs(QtWidgets.QDialog): title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') self.setWindowTitle(title) self.setObjectName('source_select_tabs') - self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setWindowIcon(build_icon(':/icon/openlp-log.svg')) self.setModal(True) self.layout = QtWidgets.QVBoxLayout() self.layout.setObjectName('source_select_tabs_layout') @@ -395,7 +395,7 @@ class SourceSelectSingle(QtWidgets.QDialog): else: title = translate('OpenLP.SourceSelectForm', 'Select Projector Source') self.setObjectName('source_select_single') - self.setWindowIcon(build_icon(':/icon/openlp-log-32x32.png')) + self.setWindowIcon(build_icon(':/icon/openlp-log.svg')) self.setModal(True) self.edit = edit diff --git a/resources/images/openlp-about-logo.png b/resources/images/openlp-about-logo.png index a20534362feca0beec7283b944f81d095062ddfc..b2b141a17c67943ec3b383236b1676860dfdb8e7 100644 GIT binary patch literal 22584 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 UP&s`hq5uE@07*qoM6N<$f@HOpYybcN diff --git a/resources/images/openlp-logo.svg b/resources/images/openlp-logo.svg index c5e7985e1..764ba8563 100644 --- a/resources/images/openlp-logo.svg +++ b/resources/images/openlp-logo.svg @@ -1,5 +1,5 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - image/svg+xml - - - - - - - - - - - - - - - - - - + inkscape:zoom="1" + inkscape:cx="151.21623" + inkscape:cy="240.04907" + inkscape:window-x="1920" + inkscape:window-y="312" + inkscape:window-maximized="1" + inkscape:current-layer="svg5740" + fit-margin-left="0" + fit-margin-top="0" + fit-margin-right="0" + fit-margin-bottom="0" + showguides="true" + inkscape:guide-bbox="true"> \ No newline at end of file diff --git a/resources/images/openlp-splash-screen.png b/resources/images/openlp-splash-screen.png index 618e47c6eb86f856f098a58fbfe5dccf9bdbb5c0..09785488a219db75fd2ce3d3be6933e13ccfe706 100644 GIT binary patch literal 48734 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 literal 57089 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
  • m`3t!*3(D_*`~|n& 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 !oCZ#Hy0fN7PniVvQ&1N{EclUyHVit{b*I^7j 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 i56}?u|9!#jzh Date: Thu, 24 Nov 2016 23:48:51 +0200 Subject: [PATCH 02/40] Add a WordProject Bible importer --- openlp/core/ui/lib/wizard.py | 1 + .../plugins/bibles/forms/bibleimportform.py | 43 +- .../bibles/lib/importers/wordproject.py | 169 +++++ openlp/plugins/bibles/lib/manager.py | 7 +- openlp/plugins/bibles/lib/mediaitem.py | 1 + openlp/plugins/media/lib/mediaitem.py | 3 +- .../bibles/test_wordprojectimport.py | 691 ++++++++++++++++++ 7 files changed, 912 insertions(+), 3 deletions(-) create mode 100644 openlp/plugins/bibles/lib/importers/wordproject.py create mode 100644 tests/functional/openlp_plugins/bibles/test_wordprojectimport.py diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 5f2321f48..68efc43c1 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -46,6 +46,7 @@ class WizardStrings(object): OSIS = 'OSIS' ZEF = 'Zefania' SWORD = 'Sword' + WordProject = 'WordProject' # These strings should need a good reason to be retranslated elsewhere. FinishedImport = translate('OpenLP.Ui', 'Finished import.') FormatLabel = translate('OpenLP.Ui', 'Format:') diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index e1e062155..d7eae281f 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -125,6 +125,7 @@ class BibleImportForm(OpenLPWizard): self.csv_verses_button.clicked.connect(self.on_csv_verses_browse_button_clicked) self.open_song_browse_button.clicked.connect(self.on_open_song_browse_button_clicked) self.zefania_browse_button.clicked.connect(self.on_zefania_browse_button_clicked) + self.wordproject_browse_button.clicked.connect(self.on_wordproject_browse_button_clicked) self.web_update_button.clicked.connect(self.on_web_update_button_clicked) self.sword_browse_button.clicked.connect(self.on_sword_browse_button_clicked) self.sword_zipbrowse_button.clicked.connect(self.on_sword_zipbrowse_button_clicked) @@ -143,7 +144,7 @@ class BibleImportForm(OpenLPWizard): self.format_label = QtWidgets.QLabel(self.select_page) self.format_label.setObjectName('FormatLabel') self.format_combo_box = QtWidgets.QComboBox(self.select_page) - self.format_combo_box.addItems(['', '', '', '', '', '']) + self.format_combo_box.addItems(['', '', '', '', '', '', '']) self.format_combo_box.setObjectName('FormatComboBox') self.format_layout.addRow(self.format_label, self.format_combo_box) self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) @@ -355,6 +356,25 @@ class BibleImportForm(OpenLPWizard): self.sword_disabled_label.setObjectName('SwordDisabledLabel') self.sword_layout.addWidget(self.sword_disabled_label) self.select_stack.addWidget(self.sword_widget) + self.wordproject_widget = QtWidgets.QWidget(self.select_page) + self.wordproject_widget.setObjectName('WordProjectWidget') + self.wordproject_layout = QtWidgets.QFormLayout(self.wordproject_widget) + self.wordproject_layout.setContentsMargins(0, 0, 0, 0) + self.wordproject_layout.setObjectName('WordProjectLayout') + self.wordproject_file_label = QtWidgets.QLabel(self.wordproject_widget) + self.wordproject_file_label.setObjectName('WordProjectFileLabel') + self.wordproject_file_layout = QtWidgets.QHBoxLayout() + self.wordproject_file_layout.setObjectName('WordProjectFileLayout') + self.wordproject_file_edit = QtWidgets.QLineEdit(self.wordproject_widget) + self.wordproject_file_edit.setObjectName('WordProjectFileEdit') + self.wordproject_file_layout.addWidget(self.wordproject_file_edit) + self.wordproject_browse_button = QtWidgets.QToolButton(self.wordproject_widget) + self.wordproject_browse_button.setIcon(self.open_icon) + self.wordproject_browse_button.setObjectName('WordProjectBrowseButton') + self.wordproject_file_layout.addWidget(self.wordproject_browse_button) + self.wordproject_layout.addRow(self.wordproject_file_label, self.wordproject_file_layout) + self.wordproject_layout.setItem(5, QtWidgets.QFormLayout.LabelRole, self.spacer) + self.select_stack.addWidget(self.wordproject_widget) self.select_page_layout.addLayout(self.select_stack) self.addPage(self.select_page) # License Page @@ -400,6 +420,7 @@ class BibleImportForm(OpenLPWizard): self.format_combo_box.setItemText(BibleFormat.OSIS, WizardStrings.OSIS) self.format_combo_box.setItemText(BibleFormat.CSV, WizardStrings.CSV) self.format_combo_box.setItemText(BibleFormat.OpenSong, WizardStrings.OS) + self.format_combo_box.setItemText(BibleFormat.WordProject, WizardStrings.WordProject) self.format_combo_box.setItemText(BibleFormat.WebDownload, translate('BiblesPlugin.ImportWizardForm', 'Web Download')) self.format_combo_box.setItemText(BibleFormat.Zefania, WizardStrings.ZEF) @@ -410,6 +431,7 @@ class BibleImportForm(OpenLPWizard): self.open_song_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_source_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Location:')) self.zefania_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) + self.wordproject_file_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Bible file:')) self.web_update_label.setText(translate('BiblesPlugin.ImportWizardForm', 'Click to download bible list')) self.web_update_button.setText(translate('BiblesPlugin.ImportWizardForm', 'Download bible list')) self.web_source_combo_box.setItemText(WebDownload.Crosswalk, translate('BiblesPlugin.ImportWizardForm', @@ -504,6 +526,12 @@ class BibleImportForm(OpenLPWizard): critical_error_message_box(UiStrings().NFSs, WizardStrings.YouSpecifyFile % WizardStrings.ZEF) self.zefania_file_edit.setFocus() return False + elif self.field('source_format') == BibleFormat.WordProject: + if not self.field('wordproject_file'): + critical_error_message_box(UiStrings().NFSs, + WizardStrings.YouSpecifyFile % WizardStrings.WordProject) + self.wordproject_file_edit.setFocus() + return False elif self.field('source_format') == BibleFormat.WebDownload: # If count is 0 the bible list has not yet been downloaded if self.web_translation_combo_box.count() == 0: @@ -627,6 +655,14 @@ class BibleImportForm(OpenLPWizard): self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.ZEF, self.zefania_file_edit, 'last directory import') + def on_wordproject_browse_button_clicked(self): + """ + Show the file open dialog for the WordProject file. + """ + # TODO: Verify format() with variable template + self.get_file_name(WizardStrings.OpenTypeFile % WizardStrings.WordProject, self.wordproject_file_edit, + 'last directory import') + def on_web_update_button_clicked(self): """ Download list of bibles from Crosswalk, BibleServer and BibleGateway. @@ -707,6 +743,7 @@ class BibleImportForm(OpenLPWizard): self.select_page.registerField('csv_versefile', self.csv_verses_edit) self.select_page.registerField('opensong_file', self.open_song_file_edit) self.select_page.registerField('zefania_file', self.zefania_file_edit) + self.select_page.registerField('wordproject_file', self.wordproject_file_edit) self.select_page.registerField('web_location', self.web_source_combo_box) self.select_page.registerField('web_biblename', self.web_translation_combo_box) self.select_page.registerField('sword_folder_path', self.sword_folder_edit) @@ -799,6 +836,10 @@ class BibleImportForm(OpenLPWizard): # Import a Zefania bible. importer = self.manager.import_bible(BibleFormat.Zefania, name=license_version, filename=self.field('zefania_file')) + elif bible_type == BibleFormat.WordProject: + # Import a WordProject bible. + importer = self.manager.import_bible(BibleFormat.WordProject, name=license_version, + filename=self.field('wordproject_file')) elif bible_type == BibleFormat.SWORD: # Import a SWORD bible. if self.sword_tab_widget.currentIndex() == self.sword_tab_widget.indexOf(self.sword_folder_tab): diff --git a/openlp/plugins/bibles/lib/importers/wordproject.py b/openlp/plugins/bibles/lib/importers/wordproject.py new file mode 100644 index 000000000..f48749fc6 --- /dev/null +++ b/openlp/plugins/bibles/lib/importers/wordproject.py @@ -0,0 +1,169 @@ +# -*- 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 # +############################################################################### +import os +import re +import logging +from codecs import open as copen +from tempfile import TemporaryDirectory +from zipfile import ZipFile + +from bs4 import BeautifulSoup, Tag, NavigableString + +from openlp.plugins.bibles.lib.bibleimport import BibleImport + +BOOK_NUMBER_PATTERN = re.compile(r'\[(\d+)\]') +REPLACE_SPACES = re.compile(r'\s{2,}') + +log = logging.getLogger(__name__) + + +class WordProjectBible(BibleImport): + """ + `WordProject `_ Bible format importer class. + """ + def _cleanup(self): + """ + Clean up after ourselves + """ + self.tmp.cleanup() + + def _unzip_file(self): + """ + Unzip the file to a temporary directory + """ + self.tmp = TemporaryDirectory() + zip_file = ZipFile(os.path.abspath(self.filename)) + zip_file.extractall(self.tmp.name) + self.base_dir = os.path.join(self.tmp.name, os.path.splitext(os.path.basename(self.filename))[0]) + + def process_books(self): + """ + Extract and create the bible books from the parsed html + + :param bible_data: parsed xml + :return: None + """ + with copen(os.path.join(self.base_dir, 'index.htm'), encoding='utf-8', errors='ignore') as index_file: + page = index_file.read() + soup = BeautifulSoup(page, 'lxml') + bible_books = soup.find('div', 'textOptions').find_all('li') + book_count = len(bible_books) + for li_book in bible_books: + log.debug(li_book) + if self.stop_import_flag: + break + # Sometimes the structure is "[1] Genesis", and sometimes it's "[1] Genesis" + if isinstance(li_book.contents[0], NavigableString) and str(li_book.contents[0]).strip(): + book_string = str(li_book.contents[0]) + book_name = str(li_book.a.contents[0]) + elif li_book.a: + book_string, book_name = str(li_book.a.contents[0]).split(' ', 1) + book_link = li_book.a['href'] + book_id = int(BOOK_NUMBER_PATTERN.search(book_string).group(1)) + book_name = book_name.strip() + db_book = self.find_and_create_book(book_name, book_count, self.language_id, book_id) + self.process_chapters(db_book, book_id, book_link) + self.session.commit() + + def process_chapters(self, db_book, book_id, book_link): + """ + Extract the chapters, and do some initial processing of the verses + + :param book: An OpenLP bible database book object + :param chapters: parsed chapters + :return: None + """ + log.debug(book_link) + book_file = os.path.join(self.base_dir, os.path.normpath(book_link)) + with copen(book_file, encoding='utf-8', errors='ignore') as f: + page = f.read() + soup = BeautifulSoup(page, 'lxml') + header_div = soup.find('div', 'textHeader') + chapters_p = header_div.find('p') + if not chapters_p: + chapters_p = soup.p + log.debug(chapters_p) + for item in chapters_p.contents: + if self.stop_import_flag: + break + if isinstance(item, Tag) and item.name in ['a', 'span']: + chapter_number = int(item.string.strip()) + self.set_current_chapter(db_book.name, chapter_number) + self.process_verses(db_book, book_id, chapter_number) + + def process_verses(self, db_book, book_number, chapter_number): + """ + Get the verses for a particular book + """ + chapter_file_name = os.path.join(self.base_dir, '{:02d}'.format(book_number), '{}.htm'.format(chapter_number)) + with copen(chapter_file_name, encoding='utf-8', errors='ignore') as chapter_file: + page = chapter_file.read() + soup = BeautifulSoup(page, 'lxml') + text_body = soup.find('div', 'textBody') + if text_body: + verses_p = text_body.find('p') + else: + verses_p = soup.find_all('p')[2] + verse_number = 0 + verse_text = '' + for item in verses_p.contents: + if self.stop_import_flag: + break + if isinstance(item, Tag) and 'verse' in item.get('class', []): + if verse_number > 0: + self.process_verse(db_book, chapter_number, verse_number, verse_text.strip()) + verse_number = int(item.string.strip()) + verse_text = '' + elif isinstance(item, NavigableString): + verse_text += str(item) + elif isinstance(item, Tag) and item.name in ['span', 'a']: + verse_text += str(item.string) + else: + log.warning('Can\'t store %s', item) + self.process_verse(db_book, chapter_number, verse_number, verse_text.strip()) + + def process_verse(self, db_book, chapter_number, verse_number, verse_text): + """ + Process a verse element + :param book: A database Book object + :param chapter_number: The chapter number to add the verses to (int) + :param element: The verse element to process. (etree element type) + :param use_milestones: set to True to process a 'milestone' verse. Defaults to False + :return: None + """ + if verse_text: + log.debug('%s %s:%s %s', db_book.name, chapter_number, verse_number, verse_text.strip()) + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + + def do_import(self, bible_name=None): + """ + Loads a Bible from file. + """ + self.log_debug('Starting WordProject import from "{name}"'.format(name=self.filename)) + self._unzip_file() + self.language_id = self.get_language_id(None, bible_name=self.filename) + result = False + if self.language_id: + self.process_books() + result = True + self._cleanup() + return result diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index fbb6fb6e7..084e24270 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -31,6 +31,7 @@ from .importers.http import HTTPBible from .importers.opensong import OpenSongBible from .importers.osis import OSISBible from .importers.zefania import ZefaniaBible +from .importers.wordproject import WordProjectBible try: from .importers.sword import SwordBible except: @@ -50,6 +51,7 @@ class BibleFormat(object): WebDownload = 3 Zefania = 4 SWORD = 5 + WordProject = 6 @staticmethod def get_class(bible_format): @@ -70,6 +72,8 @@ class BibleFormat(object): return ZefaniaBible elif bible_format == BibleFormat.SWORD: return SwordBible + elif bible_format == BibleFormat.WordProject: + return WordProjectBible else: return None @@ -84,7 +88,8 @@ class BibleFormat(object): BibleFormat.OpenSong, BibleFormat.WebDownload, BibleFormat.Zefania, - BibleFormat.SWORD + BibleFormat.SWORD, + BibleFormat.WordProject ] diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py index 9a04e5360..c9b5c6b1e 100644 --- a/openlp/plugins/bibles/lib/mediaitem.py +++ b/openlp/plugins/bibles/lib/mediaitem.py @@ -425,6 +425,7 @@ class BibleMediaItem(MediaManagerItem): verse_count = self.plugin.manager.get_verse_count_by_book_ref_id(bible, book_ref_id, 1) if verse_count == 0: self.advancedSearchButton.setEnabled(False) + log.warning('Not enough chapters in %s', book_ref_id) critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.')) else: self.advancedSearchButton.setEnabled(True) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index dc196fb59..00fccb657 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -150,7 +150,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): triggers=self.on_replace_click) if 'webkit' not in get_media_players()[0]: self.replace_action.setDisabled(True) - self.replace_action_context.setDisabled(True) + if hasattr(self, 'replace_action_context'): + self.replace_action_context.setDisabled(True) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.on_reset_click) self.media_widget = QtWidgets.QWidget(self) diff --git a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py new file mode 100644 index 000000000..45a77c50f --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py @@ -0,0 +1,691 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +This module contains tests for the WordProject Bible importer. +""" + +import os +import json +from unittest import TestCase + +from openlp.plugins.bibles.lib.importers.wordproject import WordProjectBible +from openlp.plugins.bibles.lib.db import BibleDB + +from tests.functional import MagicMock, patch, call + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) +INDEX_PAGE = """ + + + + + The Holy Bible in the English language with audio narration - KJV + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    WordProject

    +
    +
    +
    + + +
    +
    + +
    + +facebook + +twitter + +google + +linkin

    +
    + +
    + + +
    +
    +
    + +
    +
    +
    +

    Top + +

    + +
    +
    +
    + + + + + + + + +""" +CHAPTER_PAGE = """ + + + + + Creation of the world, Genesis Chapter 1 + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    WordProject

    +
    +
    +
    + + +
    +
    + +
    + +facebook + +twitter + +google + +linkin

    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +

    Genesis

    + +

    Chapter: + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 + +

    +
    + + + +
    +
    +

    Chapter 1

    + + +

    1 In the beginning God created the heaven and the earth. +
    2 And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. +
    3 And God said, Let there be light: and there was light. +
    4 And God saw the light, that it was good: and God divided the light from the darkness. +
    5 And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day. +
    6 And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters. +
    7 And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. +
    8 And God called the firmament Heaven. And the evening and the morning were the second day. +
    9 And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. +
    10 And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. +
    11 And God said, Let the earth bring forth grass, the herb yielding seed, and the fruit tree yielding fruit after his kind, whose seed is in itself, upon the earth: and it was so. +
    12 And the earth brought forth grass, and herb yielding seed after his kind, and the tree yielding fruit, whose seed was in itself, after his kind: and God saw that it was good. +
    13 And the evening and the morning were the third day. +
    14 And God said, Let there be lights in the firmament of the heaven to divide the day from the night; and let them be for signs, and for seasons, and for days, and years: +
    15 And let them be for lights in the firmament of the heaven to give light upon the earth: and it was so. +
    16 And God made two great lights; the greater light to rule the day, and the lesser light to rule the night: he made the stars also. +
    17 And God set them in the firmament of the heaven to give light upon the earth, +
    18 And to rule over the day and over the night, and to divide the light from the darkness: and God saw that it was good. +
    19 And the evening and the morning were the fourth day. +
    20 And God said, Let the waters bring forth abundantly the moving creature that hath life, and fowl that may fly above the earth in the open firmament of heaven. +
    21 And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good. +
    22 And God blessed them, saying, Be fruitful, and multiply, and fill the waters in the seas, and let fowl multiply in the earth. +
    23 And the evening and the morning were the fifth day. +
    24 And God said, Let the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the earth after his kind: and it was so. +
    25 And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good. +
    26 And God said, Let us make man in our image, after our likeness: and let them have dominion over the fish of the sea, and over the fowl of the air, and over the cattle, and over all the earth, and over every creeping thing that creepeth upon the earth. +
    27 So God created man in his own image, in the image of God created he him; male and female created he them. +
    28 And God blessed them, and God said unto them, Be fruitful, and multiply, and replenish the earth, and subdue it: and have dominion over the fish of the sea, and over the fowl of the air, and over every living thing that moveth upon the earth. +
    29 And God said, Behold, I have given you every herb bearing seed, which is upon the face of all the earth, and every tree, in the which is the fruit of a tree yielding seed; to you it shall be for meat. +
    30 And to every beast of the earth, and to every fowl of the air, and to every thing that creepeth upon the earth, wherein there is life, I have given every green herb for meat: and it was so. +
    31 And God saw every thing that he had made, and, behold, it was very good. And the evening and the morning were the sixth day. +

    + +
    +
    +
    +
    +
    + +
    +
    +
    +

     printer  +  arrowup  + +  arrowright 

    + +
    +
    +
    + + + + + + + + +""" + + +class TestWordProjectImport(TestCase): + """ + Test the functions in the :mod:`wordprojectimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry') + self.addCleanup(self.registry_patcher.stop) + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.addCleanup(self.manager_patcher.stop) + self.manager_patcher.start() + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_books(self, mocked_open, mocked_os): + """ + Test the process_books() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = INDEX_PAGE + mocked_os.path.join.side_effect = lambda *x: ''.join(x) + + # WHEN: process_books() is called + with patch.object(importer, 'find_and_create_book') as mocked_find_and_create_book, \ + patch.object(importer, 'process_chapters') as mocked_process_chapters, \ + patch.object(importer, 'session') as mocked_session: + importer.process_books() + + # THEN: The right methods should have been called + mocked_os.path.join.assert_called_once_with('', 'index.htm') + mocked_open.assert_called_once_with('index.htm', encoding='utf-8', errors='ignore') + assert mocked_find_and_create_book.call_count == 66, 'There should be 66 books' + assert mocked_process_chapters.call_count == 66, 'There should be 66 books' + assert mocked_session.commit.call_count == 66, 'There should be 66 books' + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_chapters(self, mocked_open, mocked_os): + """ + Test the process_chapters() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE + mocked_os.path.join.side_effect = lambda *x: ''.join(x) + mocked_os.path.normpath.side_effect = lambda x: x + mocked_db_book = MagicMock() + mocked_db_book.name = 'Genesis' + book_id = 1 + book_link = '01/1.htm' + + # WHEN: process_chapters() is called + with patch.object(importer, 'set_current_chapter') as mocked_set_current_chapter, \ + patch.object(importer, 'process_verses') as mocked_process_verses: + importer.process_chapters(mocked_db_book, book_id, book_link) + + # THEN: The right methods should have been called + expected_set_current_chapter_calls = [call('Genesis', ch) for ch in range(1, 51)] + expected_process_verses_calls = [call(mocked_db_book, 1, ch) for ch in range(1, 51)] + mocked_os.path.join.assert_called_once_with('', '01/1.htm') + mocked_open.assert_called_once_with('01/1.htm', encoding='utf-8', errors='ignore') + assert mocked_set_current_chapter.call_args_list == expected_set_current_chapter_calls + assert mocked_process_verses.call_args_list == expected_process_verses_calls + + @patch('openlp.plugins.bibles.lib.importers.wordproject.os') + @patch('openlp.plugins.bibles.lib.importers.wordproject.copen') + def test_process_verses(self, mocked_open, mocked_os): + """ + Test the process_verses() method + """ + # GIVEN: A WordProject importer and a bunch of mocked things + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + importer.base_dir = '' + importer.stop_import_flag = False + importer.language_id = 'en' + mocked_open.return_value.__enter__.return_value.read.return_value = CHAPTER_PAGE + mocked_os.path.join.side_effect = lambda *x: '/'.join(x) + mocked_db_book = MagicMock() + mocked_db_book.name = 'Genesis' + book_number = 1 + chapter_number = 1 + + # WHEN: process_verses() is called + with patch.object(importer, 'process_verse') as mocked_process_verse: + importer.process_verses(mocked_db_book, book_number, chapter_number) + + # THEN: All the right methods should have been called + mocked_os.path.join.assert_called_once_with('', '01', '1.htm') + mocked_open.assert_called_once_with('/01/1.htm', encoding='utf-8', errors='ignore') + assert mocked_process_verse.call_count == 31 + + def test_process_verse(self): + """ + Test the process_verse() method + """ + # GIVEN: An importer and a mocked method + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + mocked_db_book = MagicMock() + mocked_db_book.id = 1 + chapter_number = 1 + verse_number = 1 + verse_text = ' In the beginning, God created the heavens and the earth ' + + # WHEN: process_verse() is called + with patch.object(importer, 'create_verse') as mocked_create_verse: + importer.process_verse(mocked_db_book, chapter_number, verse_number, verse_text) + + # THEN: The create_verse() method should have been called + mocked_create_verse.assert_called_once_with(1, 1, 1, 'In the beginning, God created the heavens and the earth') + + def test_process_verse_no_text(self): + """ + Test the process_verse() method when there's no text + """ + # GIVEN: An importer and a mocked method + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + mocked_db_book = MagicMock() + mocked_db_book.id = 1 + chapter_number = 1 + verse_number = 1 + verse_text = '' + + # WHEN: process_verse() is called + with patch.object(importer, 'create_verse') as mocked_create_verse: + importer.process_verse(mocked_db_book, chapter_number, verse_number, verse_text) + + # THEN: The create_verse() method should NOT have been called + assert mocked_create_verse.call_count == 0 + + def test_do_import(self): + """ + Test the do_import() method + """ + # GIVEN: An importer and mocked methods + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + + # WHEN: do_import() is called + with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ + patch.object(importer, 'get_language_id') as mocked_get_language_id, \ + patch.object(importer, 'process_books') as mocked_process_books, \ + patch.object(importer, '_cleanup') as mocked_cleanup: + mocked_get_language_id.return_value = 1 + result = importer.do_import() + + # THEN: The correct methods should have been called + mocked_unzip_file.assert_called_once_with() + mocked_get_language_id.assert_called_once_with(None, bible_name='kj.zip') + mocked_process_books.assert_called_once_with() + mocked_cleanup.assert_called_once_with() + assert result is True + + def test_do_import_no_language(self): + """ + Test the do_import() method when the language is not available + """ + # GIVEN: An importer and mocked methods + importer = WordProjectBible(MagicMock(), path='.', name='.', filename='kj.zip') + + # WHEN: do_import() is called + with patch.object(importer, '_unzip_file') as mocked_unzip_file, \ + patch.object(importer, 'get_language_id') as mocked_get_language_id, \ + patch.object(importer, 'process_books') as mocked_process_books, \ + patch.object(importer, '_cleanup') as mocked_cleanup: + mocked_get_language_id.return_value = None + result = importer.do_import() + + # THEN: The correct methods should have been called + mocked_unzip_file.assert_called_once_with() + mocked_get_language_id.assert_called_once_with(None, bible_name='kj.zip') + assert mocked_process_books.call_count == 0 + mocked_cleanup.assert_called_once_with() + assert result is False From 846476c4ad00c92caa97c5974cadc9adb1ae051a Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 25 Nov 2016 15:58:33 +0200 Subject: [PATCH 03/40] Stop wizards from resizing themselves based on the contents of comboboxes --- openlp/core/ui/lib/wizard.py | 1 + .../plugins/bibles/forms/bibleimportform.py | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/openlp/core/ui/lib/wizard.py b/openlp/core/ui/lib/wizard.py index 68efc43c1..06225a376 100644 --- a/openlp/core/ui/lib/wizard.py +++ b/openlp/core/ui/lib/wizard.py @@ -96,6 +96,7 @@ class OpenLPWizard(QtWidgets.QWizard, RegistryProperties): super(OpenLPWizard, self).__init__(parent) self.plugin = plugin self.with_progress_page = add_progress_page + self.setFixedWidth(640) self.setObjectName(name) self.open_icon = build_icon(':/general/general_open.png') self.delete_icon = build_icon(':/general/general_delete.png') diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index d7eae281f..564474271 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -162,6 +162,7 @@ class BibleImportForm(OpenLPWizard): self.osis_file_layout = QtWidgets.QHBoxLayout() self.osis_file_layout.setObjectName('OsisFileLayout') self.osis_file_edit = QtWidgets.QLineEdit(self.osis_widget) + self.osis_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.osis_file_edit.setObjectName('OsisFileEdit') self.osis_file_layout.addWidget(self.osis_file_edit) self.osis_browse_button = QtWidgets.QToolButton(self.osis_widget) @@ -181,6 +182,7 @@ class BibleImportForm(OpenLPWizard): self.csv_books_layout = QtWidgets.QHBoxLayout() self.csv_books_layout.setObjectName('CsvBooksLayout') self.csv_books_edit = QtWidgets.QLineEdit(self.csv_widget) + self.csv_books_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.csv_books_edit.setObjectName('CsvBooksEdit') self.csv_books_layout.addWidget(self.csv_books_edit) self.csv_books_button = QtWidgets.QToolButton(self.csv_widget) @@ -193,6 +195,7 @@ class BibleImportForm(OpenLPWizard): self.csv_verses_layout = QtWidgets.QHBoxLayout() self.csv_verses_layout.setObjectName('CsvVersesLayout') self.csv_verses_edit = QtWidgets.QLineEdit(self.csv_widget) + self.csv_verses_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.csv_verses_edit.setObjectName('CsvVersesEdit') self.csv_verses_layout.addWidget(self.csv_verses_edit) self.csv_verses_button = QtWidgets.QToolButton(self.csv_widget) @@ -212,6 +215,7 @@ class BibleImportForm(OpenLPWizard): self.open_song_file_layout = QtWidgets.QHBoxLayout() self.open_song_file_layout.setObjectName('OpenSongFileLayout') self.open_song_file_edit = QtWidgets.QLineEdit(self.open_song_widget) + self.open_song_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.open_song_file_edit.setObjectName('OpenSongFileEdit') self.open_song_file_layout.addWidget(self.open_song_file_edit) self.open_song_browse_button = QtWidgets.QToolButton(self.open_song_widget) @@ -301,55 +305,58 @@ class BibleImportForm(OpenLPWizard): self.sword_widget = QtWidgets.QWidget(self.select_page) self.sword_widget.setObjectName('SwordWidget') self.sword_layout = QtWidgets.QVBoxLayout(self.sword_widget) + self.sword_layout.setContentsMargins(0, 0, 0, 0) self.sword_layout.setObjectName('SwordLayout') self.sword_tab_widget = QtWidgets.QTabWidget(self.sword_widget) self.sword_tab_widget.setObjectName('SwordTabWidget') self.sword_folder_tab = QtWidgets.QWidget(self.sword_tab_widget) self.sword_folder_tab.setObjectName('SwordFolderTab') - self.sword_folder_tab_layout = QtWidgets.QGridLayout(self.sword_folder_tab) + self.sword_folder_tab_layout = QtWidgets.QFormLayout(self.sword_folder_tab) self.sword_folder_tab_layout.setObjectName('SwordTabFolderLayout') self.sword_folder_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_folder_label.setObjectName('SwordSourceLabel') - self.sword_folder_tab_layout.addWidget(self.sword_folder_label, 0, 0) self.sword_folder_label.setObjectName('SwordFolderLabel') self.sword_folder_edit = QtWidgets.QLineEdit(self.sword_folder_tab) + self.sword_folder_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_folder_edit.setObjectName('SwordFolderEdit') self.sword_browse_button = QtWidgets.QToolButton(self.sword_folder_tab) self.sword_browse_button.setIcon(self.open_icon) self.sword_browse_button.setObjectName('SwordBrowseButton') - self.sword_folder_tab_layout.addWidget(self.sword_folder_edit, 0, 1) - self.sword_folder_tab_layout.addWidget(self.sword_browse_button, 0, 2) + self.sword_folder_layout = QtWidgets.QHBoxLayout() + self.sword_folder_layout.addWidget(self.sword_folder_edit) + self.sword_folder_layout.addWidget(self.sword_browse_button) + self.sword_folder_tab_layout.addRow(self.sword_folder_label, self.sword_folder_layout) self.sword_bible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_bible_label.setObjectName('SwordBibleLabel') - self.sword_folder_tab_layout.addWidget(self.sword_bible_label, 1, 0) self.sword_bible_combo_box = QtWidgets.QComboBox(self.sword_folder_tab) self.sword_bible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.sword_bible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) self.sword_bible_combo_box.setObjectName('SwordBibleComboBox') - self.sword_folder_tab_layout.addWidget(self.sword_bible_combo_box, 1, 1) + self.sword_folder_tab_layout.addRow(self.sword_bible_label, self.sword_bible_combo_box) self.sword_tab_widget.addTab(self.sword_folder_tab, '') self.sword_zip_tab = QtWidgets.QWidget(self.sword_tab_widget) self.sword_zip_tab.setObjectName('SwordZipTab') - self.sword_zip_layout = QtWidgets.QGridLayout(self.sword_zip_tab) + self.sword_zip_layout = QtWidgets.QFormLayout(self.sword_zip_tab) self.sword_zip_layout.setObjectName('SwordZipLayout') self.sword_zipfile_label = QtWidgets.QLabel(self.sword_zip_tab) self.sword_zipfile_label.setObjectName('SwordZipFileLabel') self.sword_zipfile_edit = QtWidgets.QLineEdit(self.sword_zip_tab) + self.sword_zipfile_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.sword_zipfile_edit.setObjectName('SwordZipFileEdit') self.sword_zipbrowse_button = QtWidgets.QToolButton(self.sword_zip_tab) self.sword_zipbrowse_button.setIcon(self.open_icon) self.sword_zipbrowse_button.setObjectName('SwordZipBrowseButton') + self.sword_zipfile_layout = QtWidgets.QHBoxLayout() + self.sword_zipfile_layout.addWidget(self.sword_zipfile_edit) + self.sword_zipfile_layout.addWidget(self.sword_zipbrowse_button) + self.sword_zip_layout.addRow(self.sword_zipfile_label, self.sword_zipfile_layout) self.sword_zipbible_label = QtWidgets.QLabel(self.sword_folder_tab) self.sword_zipbible_label.setObjectName('SwordZipBibleLabel') self.sword_zipbible_combo_box = QtWidgets.QComboBox(self.sword_zip_tab) self.sword_zipbible_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.sword_zipbible_combo_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) self.sword_zipbible_combo_box.setObjectName('SwordZipBibleComboBox') - self.sword_zip_layout.addWidget(self.sword_zipfile_label, 0, 0) - self.sword_zip_layout.addWidget(self.sword_zipfile_edit, 0, 1) - self.sword_zip_layout.addWidget(self.sword_zipbrowse_button, 0, 2) - self.sword_zip_layout.addWidget(self.sword_zipbible_label, 1, 0) - self.sword_zip_layout.addWidget(self.sword_zipbible_combo_box, 1, 1) + self.sword_zip_layout.addRow(self.sword_zipbible_label, self.sword_zipbible_combo_box) self.sword_tab_widget.addTab(self.sword_zip_tab, '') self.sword_layout.addWidget(self.sword_tab_widget) self.sword_disabled_label = QtWidgets.QLabel(self.sword_widget) @@ -366,6 +373,7 @@ class BibleImportForm(OpenLPWizard): self.wordproject_file_layout = QtWidgets.QHBoxLayout() self.wordproject_file_layout.setObjectName('WordProjectFileLayout') self.wordproject_file_edit = QtWidgets.QLineEdit(self.wordproject_widget) + self.wordproject_file_edit.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) self.wordproject_file_edit.setObjectName('WordProjectFileEdit') self.wordproject_file_layout.addWidget(self.wordproject_file_edit) self.wordproject_browse_button = QtWidgets.QToolButton(self.wordproject_widget) @@ -490,6 +498,7 @@ class BibleImportForm(OpenLPWizard): """ Validate the current page before moving on to the next page. """ + log.debug(self.size()) if self.currentPage() == self.welcome_page: return True elif self.currentPage() == self.select_page: From d0ed37e1d592ae5a41585e028a7a8a74c662d5a4 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 25 Nov 2016 16:17:34 +0200 Subject: [PATCH 04/40] Move contents of html files into actual html files --- .../bibles/test_wordprojectimport.py | 475 +----------------- .../resources/bibles/wordproject_chapter.htm | 248 +++++++++ tests/resources/bibles/wordproject_index.htm | 222 ++++++++ 3 files changed, 472 insertions(+), 473 deletions(-) create mode 100644 tests/resources/bibles/wordproject_chapter.htm create mode 100644 tests/resources/bibles/wordproject_index.htm diff --git a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py index 45a77c50f..622f83fa8 100644 --- a/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py +++ b/tests/functional/openlp_plugins/bibles/test_wordprojectimport.py @@ -34,479 +34,8 @@ from tests.functional import MagicMock, patch, call TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'bibles')) -INDEX_PAGE = """ - - - - - The Holy Bible in the English language with audio narration - KJV - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -

    WordProject

    -
    -
    -
    - - -
    -
    - -
    - -facebook - -twitter - -google - -linkin

    -
    - -
    - - -
    -
    -
    - -
    -
    -
    -

    Top - -

    - -
    -
    -
    - - - - - - - - -""" -CHAPTER_PAGE = """ - - - - - Creation of the world, Genesis Chapter 1 - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -

    WordProject

    -
    -
    -
    - - -
    -
    - -
    - -facebook - -twitter - -google - -linkin

    -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -

    Genesis

    - -

    Chapter: - -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 - -

    -
    - - - -
    -
    -

    Chapter 1

    - - -

    1 In the beginning God created the heaven and the earth. -
    2 And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. -
    3 And God said, Let there be light: and there was light. -
    4 And God saw the light, that it was good: and God divided the light from the darkness. -
    5 And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day. -
    6 And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters. -
    7 And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. -
    8 And God called the firmament Heaven. And the evening and the morning were the second day. -
    9 And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. -
    10 And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. -
    11 And God said, Let the earth bring forth grass, the herb yielding seed, and the fruit tree yielding fruit after his kind, whose seed is in itself, upon the earth: and it was so. -
    12 And the earth brought forth grass, and herb yielding seed after his kind, and the tree yielding fruit, whose seed was in itself, after his kind: and God saw that it was good. -
    13 And the evening and the morning were the third day. -
    14 And God said, Let there be lights in the firmament of the heaven to divide the day from the night; and let them be for signs, and for seasons, and for days, and years: -
    15 And let them be for lights in the firmament of the heaven to give light upon the earth: and it was so. -
    16 And God made two great lights; the greater light to rule the day, and the lesser light to rule the night: he made the stars also. -
    17 And God set them in the firmament of the heaven to give light upon the earth, -
    18 And to rule over the day and over the night, and to divide the light from the darkness: and God saw that it was good. -
    19 And the evening and the morning were the fourth day. -
    20 And God said, Let the waters bring forth abundantly the moving creature that hath life, and fowl that may fly above the earth in the open firmament of heaven. -
    21 And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good. -
    22 And God blessed them, saying, Be fruitful, and multiply, and fill the waters in the seas, and let fowl multiply in the earth. -
    23 And the evening and the morning were the fifth day. -
    24 And God said, Let the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the earth after his kind: and it was so. -
    25 And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good. -
    26 And God said, Let us make man in our image, after our likeness: and let them have dominion over the fish of the sea, and over the fowl of the air, and over the cattle, and over all the earth, and over every creeping thing that creepeth upon the earth. -
    27 So God created man in his own image, in the image of God created he him; male and female created he them. -
    28 And God blessed them, and God said unto them, Be fruitful, and multiply, and replenish the earth, and subdue it: and have dominion over the fish of the sea, and over the fowl of the air, and over every living thing that moveth upon the earth. -
    29 And God said, Behold, I have given you every herb bearing seed, which is upon the face of all the earth, and every tree, in the which is the fruit of a tree yielding seed; to you it shall be for meat. -
    30 And to every beast of the earth, and to every fowl of the air, and to every thing that creepeth upon the earth, wherein there is life, I have given every green herb for meat: and it was so. -
    31 And God saw every thing that he had made, and, behold, it was very good. And the evening and the morning were the sixth day. -

    - -
    -
    -
    -
    -
    - -
    -
    -
    -

     printer  -  arrowup  - -  arrowright 

    - -
    -
    -
    - - - - - - - - -""" +INDEX_PAGE = open(os.path.join(TEST_PATH, 'wordproject_index.htm')).read() +CHAPTER_PAGE = open(os.path.join(TEST_PATH, 'wordproject_chapter.htm')).read() class TestWordProjectImport(TestCase): diff --git a/tests/resources/bibles/wordproject_chapter.htm b/tests/resources/bibles/wordproject_chapter.htm new file mode 100644 index 000000000..fb9b8a272 --- /dev/null +++ b/tests/resources/bibles/wordproject_chapter.htm @@ -0,0 +1,248 @@ + + + + + Creation of the world, Genesis Chapter 1 + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    WordProject

    +
    +
    +
    + + +
    +
    + +
    + +facebook + +twitter + +google + +linkin

    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +

    Genesis

    + +

    Chapter: + +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 + +

    +
    + + + +
    +
    +

    Chapter 1

    + + +

    1 In the beginning God created the heaven and the earth. +
    2 And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. +
    3 And God said, Let there be light: and there was light. +
    4 And God saw the light, that it was good: and God divided the light from the darkness. +
    5 And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day. +
    6 And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters. +
    7 And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. +
    8 And God called the firmament Heaven. And the evening and the morning were the second day. +
    9 And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. +
    10 And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. +
    11 And God said, Let the earth bring forth grass, the herb yielding seed, and the fruit tree yielding fruit after his kind, whose seed is in itself, upon the earth: and it was so. +
    12 And the earth brought forth grass, and herb yielding seed after his kind, and the tree yielding fruit, whose seed was in itself, after his kind: and God saw that it was good. +
    13 And the evening and the morning were the third day. +
    14 And God said, Let there be lights in the firmament of the heaven to divide the day from the night; and let them be for signs, and for seasons, and for days, and years: +
    15 And let them be for lights in the firmament of the heaven to give light upon the earth: and it was so. +
    16 And God made two great lights; the greater light to rule the day, and the lesser light to rule the night: he made the stars also. +
    17 And God set them in the firmament of the heaven to give light upon the earth, +
    18 And to rule over the day and over the night, and to divide the light from the darkness: and God saw that it was good. +
    19 And the evening and the morning were the fourth day. +
    20 And God said, Let the waters bring forth abundantly the moving creature that hath life, and fowl that may fly above the earth in the open firmament of heaven. +
    21 And God created great whales, and every living creature that moveth, which the waters brought forth abundantly, after their kind, and every winged fowl after his kind: and God saw that it was good. +
    22 And God blessed them, saying, Be fruitful, and multiply, and fill the waters in the seas, and let fowl multiply in the earth. +
    23 And the evening and the morning were the fifth day. +
    24 And God said, Let the earth bring forth the living creature after his kind, cattle, and creeping thing, and beast of the earth after his kind: and it was so. +
    25 And God made the beast of the earth after his kind, and cattle after their kind, and every thing that creepeth upon the earth after his kind: and God saw that it was good. +
    26 And God said, Let us make man in our image, after our likeness: and let them have dominion over the fish of the sea, and over the fowl of the air, and over the cattle, and over all the earth, and over every creeping thing that creepeth upon the earth. +
    27 So God created man in his own image, in the image of God created he him; male and female created he them. +
    28 And God blessed them, and God said unto them, Be fruitful, and multiply, and replenish the earth, and subdue it: and have dominion over the fish of the sea, and over the fowl of the air, and over every living thing that moveth upon the earth. +
    29 And God said, Behold, I have given you every herb bearing seed, which is upon the face of all the earth, and every tree, in the which is the fruit of a tree yielding seed; to you it shall be for meat. +
    30 And to every beast of the earth, and to every fowl of the air, and to every thing that creepeth upon the earth, wherein there is life, I have given every green herb for meat: and it was so. +
    31 And God saw every thing that he had made, and, behold, it was very good. And the evening and the morning were the sixth day. +

    + +
    +
    +
    +
    +
    + +
    +
    +
    +

     printer  +  arrowup  + +  arrowright 

    + +
    +
    +
    + + + + + + + + + diff --git a/tests/resources/bibles/wordproject_index.htm b/tests/resources/bibles/wordproject_index.htm new file mode 100644 index 000000000..861ca2dda --- /dev/null +++ b/tests/resources/bibles/wordproject_index.htm @@ -0,0 +1,222 @@ + + + + + The Holy Bible in the English language with audio narration - KJV + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +

    WordProject

    +
    +
    +
    + + +
    +
    + +
    + +facebook + +twitter + +google + +linkin

    +
    + +
    + + +
    +
    +
    + +
    +
    +
    +

    Top + +

    + +
    +
    +
    + + + + + + + + From ef7f0b13892bcee190fd9189ad412a27726b37d8 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 25 Nov 2016 16:21:27 +0200 Subject: [PATCH 05/40] Fix pep8 --- openlp/plugins/bibles/forms/bibleimportform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py index 564474271..ae39d821b 100644 --- a/openlp/plugins/bibles/forms/bibleimportform.py +++ b/openlp/plugins/bibles/forms/bibleimportform.py @@ -538,7 +538,7 @@ class BibleImportForm(OpenLPWizard): elif self.field('source_format') == BibleFormat.WordProject: if not self.field('wordproject_file'): critical_error_message_box(UiStrings().NFSs, - WizardStrings.YouSpecifyFile % WizardStrings.WordProject) + WizardStrings.YouSpecifyFile % WizardStrings.WordProject) self.wordproject_file_edit.setFocus() return False elif self.field('source_format') == BibleFormat.WebDownload: From 595fd90d45691e508504c0286456531c5ec6dad5 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 14:06:54 +0100 Subject: [PATCH 06/40] Fix some errors on windows --- tests/functional/openlp_core_ui/test_maindisplay.py | 1 + .../openlp_plugins/presentations/test_powerpointcontroller.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 5aa0f42a4..1ac6160a6 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -270,6 +270,7 @@ class TestMainDisplay(TestCase, TestMixin): service_item = MagicMock() service_item.theme_data = MagicMock() service_item.theme_data.background_type = 'video' + service_item.theme_data.theme_name = 'name' mocked_plugin = MagicMock() display.plugin_manager = PluginManager() display.plugin_manager.plugins = [mocked_plugin] diff --git a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py index 3666eac40..824951a66 100644 --- a/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py +++ b/tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py @@ -137,7 +137,7 @@ class TestPowerpointDocument(TestCase, TestMixin): instance.goto_slide(42) # THEN: mocked_critical_error_message_box should have been called - mocked_critical_error_message_box.assert_called_with('Error', 'An error occurred in the Powerpoint ' + mocked_critical_error_message_box.assert_called_with('Error', 'An error occurred in the PowerPoint ' 'integration and the presentation will be stopped.' ' Restart the presentation if you wish to ' 'present it.') From ceffeae39c6faf70b061944a86bb590c4da39b9e Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 14:07:21 +0100 Subject: [PATCH 07/40] Added appveyor conf file. --- appveyor.yml | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..8d3f61477 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,66 @@ +version: 2.5.0.{build} + +init: + - choco install -y --force bzr + - set PATH=C:\Program Files (x86)\Bazaar;%PATH% + - bzr --version + +clone_script: + - bzr checkout --lightweight lp:openlp openlp + +environment: + PYTHON: C:\\Python34 + +install: + # Install dependencies from pypi + - %PYTHON%\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==3.0.10 psycopg2 pypiwin32 pyenchant + # Install mysql dependency + - %PYTHON%\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df + # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) + - curl -L "https://www.dropbox.com/s/7dwwna459j6qvbp/lxml-3.6.4-cp34-cp34m-win32.whl?dl=1" -o lxml-3.6.4-cp34-cp34m-win32.whl + - %PYTHON%\python.exe -m pip install lxml-3.6.4-cp34-cp34m-win32.whl + - curl -L "https://www.dropbox.com/s/ib1yq4xq7o1dma7/PyICU-1.9.5-cp34-cp34m-win32.whl?dl=1" -o PyICU-1.9.5-cp34-cp34m-win32.whl + - %PYTHON%\python.exe -m pip install PyICU-1.9.5-cp34-cp34m-win32.whl + # Download and install PyQt5 + - curl -L -O http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe + - PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S + # Download and install Inno Setup - used for packaging + - curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe + - is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP- + # Download and unpack portable-bundle + - curl -L "https://www.dropbox.com/s/omr8mw9kamnml3l/portable-setup.7z?dl=1" -o portable-setup.7z + - 7z x portable-setup.7z + # Download and unpack mupdf + - curl -O http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip + - 7z x mupdf-1.9a-windows.zip + - cp mupdf-1.9a-windows/mupdf.exe openlp/mupdf.exe + # Download and unpack mediainfo + - curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip + - mkdir MediaInfo + - 7z x -o MediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip + - cp MediaInfo\\MediaInfo.exe openlp\\MediaInfo.exe + # Disabled portable installers - can't figure out how to make them silent + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe + # - PortableApps.comInstaller_3.4.4.paf.exe /S + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Launcher/PortableApps.comLauncher_2.2.1.paf.exe + # - PortableApps.comLauncher_2.2.1.paf.exe /S + # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe + # - NSISPortable_3.0_English.paf.exe /S + + +build: off + +test_script: + - cd openlp + - %PYTHON%\\python.exe -m nose -v tests + +after_test: + # This is where we create a package using PyInstaller + # First download and unpack PyInstaller + - curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip + - 7z x develop.zip + # Then get the packaging repo + - bzr checkout --lightweight lp:~tomasgroth/openlp/packaging-appveyor packaging + - cd packaging + - %PYTHON%\python.exe windows/windows-builder.py -v -u -t -c windows\\config.ini -b ..\\openlp + From 7017f82b3617890cd632d16b9c6b37f2a87113f8 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 14:17:32 +0100 Subject: [PATCH 08/40] Workaround for 7z extraction --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 8d3f61477..189d94151 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,7 +37,9 @@ install: # Download and unpack mediainfo - curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip - mkdir MediaInfo - - 7z x -o MediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip + - cd MediaInfo + - 7z x ../MediaInfo_CLI_0.7.90_Windows_i386.zip + - cd.. - cp MediaInfo\\MediaInfo.exe openlp\\MediaInfo.exe # Disabled portable installers - can't figure out how to make them silent # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe From 8595a2171f6feb4751b81577f922b47c6a588f60 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 14:54:29 +0100 Subject: [PATCH 09/40] Another test fix for windows. --- tests/functional/openlp_core_ui/test_maindisplay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 1ac6160a6..751263e06 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -271,6 +271,7 @@ class TestMainDisplay(TestCase, TestMixin): service_item.theme_data = MagicMock() service_item.theme_data.background_type = 'video' service_item.theme_data.theme_name = 'name' + service_item._raw_frames = [] mocked_plugin = MagicMock() display.plugin_manager = PluginManager() display.plugin_manager.plugins = [mocked_plugin] From ccdc88c4cbcb60a567c7eb913d56c4f3d9423ed5 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 29 Nov 2016 17:44:32 +0200 Subject: [PATCH 10/40] Change the name of the help menu to try to fix the double-help-menu problem --- openlp/core/common/settings.py | 6 +++--- openlp/core/ui/mainwindow.py | 11 +++++------ openlp/plugins/media/lib/mediaitem.py | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 8f70fafff..132e9652a 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -216,8 +216,8 @@ class Settings(QtCore.QSettings): ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4. ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4. ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6. - ('shortcuts/offlineHelpItem', 'shortcuts/HelpItem', []), # Online and Offline help were combined in 2.6. - ('shortcuts/onlineHelpItem', 'shortcuts/HelpItem', []) # Online and Offline help were combined in 2.6. + ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6. + ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []) # Online and Offline help were combined in 2.6. ] @staticmethod @@ -276,7 +276,7 @@ class Settings(QtCore.QSettings): 'shortcuts/fileSaveItem': [QtGui.QKeySequence(QtGui.QKeySequence.Save)], 'shortcuts/fileOpenItem': [QtGui.QKeySequence(QtGui.QKeySequence.Open)], 'shortcuts/goLive': [], - 'shortcuts/HelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)], + 'shortcuts/userManualItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)], 'shortcuts/importThemeItem': [], 'shortcuts/importBibleItem': [], 'shortcuts/listViewBiblesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)], diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index b8bd126dd..e6d0634b4 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -312,10 +312,9 @@ class Ui_MainWindow(object): elif is_macosx(): self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), '..', 'Resources', 'OpenLP.help') - self.on_help_item = create_action(main_window, 'HelpItem', - icon=':/system/system_help_contents.png', - can_shortcuts=True, - category=UiStrings().Help, triggers=self.on_help_clicked) + self.user_manual_item = create_action(main_window, 'userManualItem', icon=':/system/system_help_contents.png', + can_shortcuts=True, category=UiStrings().Help, + triggers=self.on_help_clicked) self.web_site_item = create_action(main_window, 'webSiteItem', can_shortcuts=True, category=UiStrings().Help) # Shortcuts not connected to buttons or menu entries. self.search_shortcut_action = create_action(main_window, @@ -354,7 +353,7 @@ class Ui_MainWindow(object): add_actions(self.tools_menu, (self.tools_open_data_folder, None)) add_actions(self.tools_menu, (self.tools_first_time_wizard, None)) add_actions(self.tools_menu, [self.update_theme_images]) - add_actions(self.help_menu, (self.on_help_item, None, self.web_site_item, self.about_item)) + add_actions(self.help_menu, (self.user_manual_item, None, self.web_site_item, self.about_item)) add_actions(self.menu_bar, (self.file_menu.menuAction(), self.view_menu.menuAction(), self.tools_menu.menuAction(), self.settings_menu.menuAction(), self.help_menu.menuAction())) add_actions(self, [self.search_shortcut_action]) @@ -450,7 +449,7 @@ class Ui_MainWindow(object): 'from here.')) self.about_item.setText(translate('OpenLP.MainWindow', '&About')) self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP.')) - self.on_help_item.setText(translate('OpenLP.MainWindow', '&User Manual')) + self.user_manual_item.setText(translate('OpenLP.MainWindow', '&User Manual')) self.search_shortcut_action.setText(UiStrings().Search) self.search_shortcut_action.setToolTip( translate('OpenLP.MainWindow', 'Jump to the search box of the current active plugin.')) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index dc196fb59..2344bc6d6 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -150,7 +150,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): triggers=self.on_replace_click) if 'webkit' not in get_media_players()[0]: self.replace_action.setDisabled(True) - self.replace_action_context.setDisabled(True) + # self.replace_action_context.setDisabled(True) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.on_reset_click) self.media_widget = QtWidgets.QWidget(self) From 1f4c8e742ee88087cea3aceadabf771514239a98 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 21:37:39 +0100 Subject: [PATCH 11/40] More work on appveyor --- scripts/appveyor-webhook.py | 102 +++++++++++++++++++++++++++ appveyor.yml => scripts/appveyor.yml | 27 +++---- 2 files changed, 113 insertions(+), 16 deletions(-) create mode 100755 scripts/appveyor-webhook.py rename appveyor.yml => scripts/appveyor.yml (70%) diff --git a/scripts/appveyor-webhook.py b/scripts/appveyor-webhook.py new file mode 100755 index 000000000..841c5b532 --- /dev/null +++ b/scripts/appveyor-webhook.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- 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 # +############################################################################### + +import json +import urllib +import urllib.request +import datetime +import sys +from subprocess import Popen, PIPE + +token = 'xx' +webhook_url = 'https://ci.appveyor.com/api/subversion/webhook?id=x' +branch = 'lp:openlp' + +webhook_element = \ +{ + "commit": { + "author": { + "email": "open@contributer", + "name": "OpenLP Contributor" + }, + "id": None, + "message": "Building " + branch, + "timestamp": datetime.datetime.now().isoformat() + }, + "config": None, + "repository": { + "name": "repo_name", + "url": "repo_url" + } +} + + +def get_version(): + """ + Get the version of the branch. + """ + bzr = Popen(('bzr', 'tags'), stdout=PIPE) + output = bzr.communicate()[0] + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr tags') + lines = output.splitlines() + if len(lines) == 0: + tag = '0.0.0' + revision = '0' + else: + tag, revision = lines[-1].decode('utf-8').split() + bzr = Popen(('bzr', 'log', '--line', '-r', '-1'), stdout=PIPE) + output, error = bzr.communicate() + code = bzr.wait() + if code != 0: + raise Exception('Error running bzr log') + latest = output.decode('utf-8').split(':')[0] + version_string = latest == revision and tag or '%s-bzr%s' % (tag, latest) + # Save decimal version in case we need to do a portable build. + version = latest == revision and tag or '%s.%s' % (tag, latest) + return version_string, version + + +def get_yml(): + f = open('appveyor.yml') + yml_text = f.read() + f.close() + yml_text = yml_text.replace('BRANCHNAME', branch) + return yml_text + + +def hook(token, webhook_url): + webhook_element['config'] = get_yml() + webhook_element['commit']['message'] = 'Building ' + branch + version_string, version = get_version() + webhook_element['commit']['id'] = version_string + request = urllib.request.Request(webhook_url) + print(json.dumps(webhook_element)) + request.add_header('Content-Type','application/json;charset=utf-8') + request.add_header('Authorization', 'Bearer ' + token) + responce = urllib.request.urlopen(request, json.dumps(webhook_element).encode('utf-8')) + print(responce.read().decode('utf-8')) + + +hook(token, webhook_url) diff --git a/appveyor.yml b/scripts/appveyor.yml similarity index 70% rename from appveyor.yml rename to scripts/appveyor.yml index 189d94151..490464044 100644 --- a/appveyor.yml +++ b/scripts/appveyor.yml @@ -6,21 +6,21 @@ init: - bzr --version clone_script: - - bzr checkout --lightweight lp:openlp openlp + - bzr checkout --lightweight BRANCHNAME openlp environment: PYTHON: C:\\Python34 install: # Install dependencies from pypi - - %PYTHON%\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==3.0.10 psycopg2 pypiwin32 pyenchant + - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==3.0.10 psycopg2 pypiwin32 pyenchant" # Install mysql dependency - - %PYTHON%\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df + - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df" # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) - curl -L "https://www.dropbox.com/s/7dwwna459j6qvbp/lxml-3.6.4-cp34-cp34m-win32.whl?dl=1" -o lxml-3.6.4-cp34-cp34m-win32.whl - - %PYTHON%\python.exe -m pip install lxml-3.6.4-cp34-cp34m-win32.whl + - "%PYTHON%\\python.exe -m pip install lxml-3.6.4-cp34-cp34m-win32.whl" - curl -L "https://www.dropbox.com/s/ib1yq4xq7o1dma7/PyICU-1.9.5-cp34-cp34m-win32.whl?dl=1" -o PyICU-1.9.5-cp34-cp34m-win32.whl - - %PYTHON%\python.exe -m pip install PyICU-1.9.5-cp34-cp34m-win32.whl + - "%PYTHON%\\python.exe -m pip install PyICU-1.9.5-cp34-cp34m-win32.whl" # Download and install PyQt5 - curl -L -O http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe - PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S @@ -37,9 +37,7 @@ install: # Download and unpack mediainfo - curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip - mkdir MediaInfo - - cd MediaInfo - - 7z x ../MediaInfo_CLI_0.7.90_Windows_i386.zip - - cd.. + - 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip - cp MediaInfo\\MediaInfo.exe openlp\\MediaInfo.exe # Disabled portable installers - can't figure out how to make them silent # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe @@ -54,15 +52,12 @@ build: off test_script: - cd openlp - - %PYTHON%\\python.exe -m nose -v tests + - "%PYTHON%\\python.exe -m nose -v tests" after_test: # This is where we create a package using PyInstaller - # First download and unpack PyInstaller - - curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip + # First get PyInstaller + - curl -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip - 7z x develop.zip - # Then get the packaging repo - - bzr checkout --lightweight lp:~tomasgroth/openlp/packaging-appveyor packaging - - cd packaging - - %PYTHON%\python.exe windows/windows-builder.py -v -u -t -c windows\\config.ini -b ..\\openlp - + # Build the bundle + - "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp" From 1bb2e9e2781b2060f8e3ef2c137add272e01fee8 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 21:57:33 +0100 Subject: [PATCH 12/40] Another fix for tests on windows. --- tests/functional/openlp_core_ui/test_maindisplay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_ui/test_maindisplay.py b/tests/functional/openlp_core_ui/test_maindisplay.py index 751263e06..8b10f6e0f 100644 --- a/tests/functional/openlp_core_ui/test_maindisplay.py +++ b/tests/functional/openlp_core_ui/test_maindisplay.py @@ -271,7 +271,7 @@ class TestMainDisplay(TestCase, TestMixin): service_item.theme_data = MagicMock() service_item.theme_data.background_type = 'video' service_item.theme_data.theme_name = 'name' - service_item._raw_frames = [] + service_item.theme_data.background_filename = 'background_filename' mocked_plugin = MagicMock() display.plugin_manager = PluginManager() display.plugin_manager.plugins = [mocked_plugin] From bbda32b949e88848bb9772f678504423e337fceb Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 29 Nov 2016 22:08:32 +0100 Subject: [PATCH 13/40] use revision number for appveyor id --- scripts/appveyor-webhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/appveyor-webhook.py b/scripts/appveyor-webhook.py index 841c5b532..e28fac6b3 100755 --- a/scripts/appveyor-webhook.py +++ b/scripts/appveyor-webhook.py @@ -72,7 +72,7 @@ def get_version(): if code != 0: raise Exception('Error running bzr log') latest = output.decode('utf-8').split(':')[0] - version_string = latest == revision and tag or '%s-bzr%s' % (tag, latest) + version_string = latest == revision and tag or 'r%s' % latest # Save decimal version in case we need to do a portable build. version = latest == revision and tag or '%s.%s' % (tag, latest) return version_string, version @@ -93,7 +93,7 @@ def hook(token, webhook_url): webhook_element['commit']['id'] = version_string request = urllib.request.Request(webhook_url) print(json.dumps(webhook_element)) - request.add_header('Content-Type','application/json;charset=utf-8') + request.add_header('Content-Type', 'application/json;charset=utf-8') request.add_header('Authorization', 'Bearer ' + token) responce = urllib.request.urlopen(request, json.dumps(webhook_element).encode('utf-8')) print(responce.read().decode('utf-8')) From 955fdc50ac486e4bc0d2ee4b5a054e6c80822545 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 29 Nov 2016 23:57:27 +0200 Subject: [PATCH 14/40] Fix bug #1645867 by setting an application attribute related to OpenGL Fixes: https://launchpad.net/bugs/1645867, https://launchpad.net/bugs/1591749 --- openlp/core/__init__.py | 1 + openlp/core/ui/themewizard.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 5de5e69de..cb298e11b 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -375,6 +375,7 @@ def main(args=None): application.setOrganizationName('OpenLP') application.setOrganizationDomain('openlp.org') application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) + application.setAttribute(QtCore.Qt.AA_DontCreateNativeWidgetSiblings, True) if args and args.portable: application.setApplicationName('OpenLPPortable') Settings.setDefaultFormat(Settings.IniFormat) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 95262cf8f..7eac787d9 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -44,9 +44,9 @@ class Ui_ThemeWizard(object): theme_wizard.setModal(True) theme_wizard.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage | QtWidgets.QWizard.HaveCustomButton1) + theme_wizard.setFixedWidth(640) if is_macosx(): theme_wizard.setPixmap(QtWidgets.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - theme_wizard.resize(646, 400) else: theme_wizard.setWizardStyle(QtWidgets.QWizard.ModernStyle) self.spacer = QtWidgets.QSpacerItem(10, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) From 105edf36c36092c98a3e5e602b43ffe8c2cce148 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 30 Nov 2016 21:54:06 +0100 Subject: [PATCH 15/40] More appveyor adjustments --- scripts/appveyor-webhook.py | 79 ++++++++++++++++++++++++++----------- scripts/appveyor.yml | 66 +++++++++++++++++-------------- 2 files changed, 92 insertions(+), 53 deletions(-) diff --git a/scripts/appveyor-webhook.py b/scripts/appveyor-webhook.py index e28fac6b3..573aa1045 100755 --- a/scripts/appveyor-webhook.py +++ b/scripts/appveyor-webhook.py @@ -21,34 +21,40 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### +""" +This script is used to trigger a build at appveyor. Since the code is not hosted +on github the normal triggering mechanisms can't be use. The project is +registered as subversion repository. A webhook is used to trigger new builds. +The appveyor.yml used for the build is send to appveyor when calling the hook. +""" import json import urllib import urllib.request import datetime import sys +import time from subprocess import Popen, PIPE -token = 'xx' -webhook_url = 'https://ci.appveyor.com/api/subversion/webhook?id=x' -branch = 'lp:openlp' +appveyor_build_url = 'https://ci.appveyor.com/project/TomasGroth/openlp/build' +appveyor_api_url = 'https://ci.appveyor.com/api/projects/TomasGroth/openlp' webhook_element = \ -{ - "commit": { - "author": { - "email": "open@contributer", - "name": "OpenLP Contributor" + { + 'commit': { + 'author': { + 'email': 'open@contributer', + 'name': 'OpenLP Contributor' + }, + 'id': None, + 'message': None, + 'timestamp': datetime.datetime.now().isoformat() }, - "id": None, - "message": "Building " + branch, - "timestamp": datetime.datetime.now().isoformat() - }, - "config": None, - "repository": { - "name": "repo_name", - "url": "repo_url" + 'config': None, + 'repository': { + 'name': 'repo_name', + 'url': 'repo_url' + } } -} def get_version(): @@ -78,7 +84,10 @@ def get_version(): return version_string, version -def get_yml(): +def get_yml(branch): + """ + Returns the content of appveyor.yml and inserts the branch to be build + """ f = open('appveyor.yml') yml_text = f.read() f.close() @@ -86,17 +95,39 @@ def get_yml(): return yml_text -def hook(token, webhook_url): - webhook_element['config'] = get_yml() +def hook(webhook_url, yml): + """ + Activate the webhook to start the build + """ + webhook_element['config'] = yml webhook_element['commit']['message'] = 'Building ' + branch version_string, version = get_version() webhook_element['commit']['id'] = version_string request = urllib.request.Request(webhook_url) - print(json.dumps(webhook_element)) request.add_header('Content-Type', 'application/json;charset=utf-8') - request.add_header('Authorization', 'Bearer ' + token) responce = urllib.request.urlopen(request, json.dumps(webhook_element).encode('utf-8')) - print(responce.read().decode('utf-8')) + if responce.getcode() != 204: + print('An error happened when calling the webhook! Return code: %d' % responce.getcode()) + print(responce.read().decode('utf-8')) -hook(token, webhook_url) +def get_appveyor_build_url(branch): + """ + Get the url of the build. + """ + # Wait 10 seconds to make sure the hook has been triggered + time.sleep(10) + responce = urllib.request.urlopen(appveyor_api_url) + json_str = responce.read().decode('utf-8') + build_json = json.loads(json_str) + build_url = '%s/%s' % (appveyor_build_url, build_json['build']['version']) + print('Check this URL for build status: %s' % build_url) + + +if len(sys.argv) != 3: + print('Usage: %s ' % sys.argv[0]) +else: + webhook_url = sys.argv[1] + branch = sys.argv[2] + hook(webhook_url, get_yml(branch)) + get_appveyor_build_url(branch) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 490464044..4784ed50d 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -1,4 +1,4 @@ -version: 2.5.0.{build} +version: OpenLP-win-ci-b{build} init: - choco install -y --force bzr @@ -6,39 +6,54 @@ init: - bzr --version clone_script: - - bzr checkout --lightweight BRANCHNAME openlp + - bzr checkout --lightweight BRANCHNAME openlp-branch environment: PYTHON: C:\\Python34 install: # Install dependencies from pypi - - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc==3.0.10 psycopg2 pypiwin32 pyenchant" + - "%PYTHON%\\python.exe -m pip install sqlalchemy alembic chardet beautifulsoup4 Mako nose mock pyodbc psycopg2 pypiwin32 pyenchant" # Install mysql dependency - "%PYTHON%\\python.exe -m pip install http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df" # Download and install lxml and pyicu (originally from http://www.lfd.uci.edu/~gohlke/pythonlibs/) - - curl -L "https://www.dropbox.com/s/7dwwna459j6qvbp/lxml-3.6.4-cp34-cp34m-win32.whl?dl=1" -o lxml-3.6.4-cp34-cp34m-win32.whl - - "%PYTHON%\\python.exe -m pip install lxml-3.6.4-cp34-cp34m-win32.whl" - - curl -L "https://www.dropbox.com/s/ib1yq4xq7o1dma7/PyICU-1.9.5-cp34-cp34m-win32.whl?dl=1" -o PyICU-1.9.5-cp34-cp34m-win32.whl - - "%PYTHON%\\python.exe -m pip install PyICU-1.9.5-cp34-cp34m-win32.whl" + - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/lxml-3.6.4-cp34-cp34m-win32.whl" + - "%PYTHON%\\python.exe -m pip install https://get.openlp.org/win-sdk/PyICU-1.9.5-cp34-cp34m-win32.whl" # Download and install PyQt5 - curl -L -O http://downloads.sourceforge.net/project/pyqt/PyQt5/PyQt-5.5.1/PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe - PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe /S - # Download and install Inno Setup - used for packaging - - curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe - - is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP- - # Download and unpack portable-bundle - - curl -L "https://www.dropbox.com/s/omr8mw9kamnml3l/portable-setup.7z?dl=1" -o portable-setup.7z - - 7z x portable-setup.7z # Download and unpack mupdf - curl -O http://mupdf.com/downloads/archive/mupdf-1.9a-windows.zip - 7z x mupdf-1.9a-windows.zip - - cp mupdf-1.9a-windows/mupdf.exe openlp/mupdf.exe + - cp mupdf-1.9a-windows/mupdf.exe openlp-branch/mupdf.exe # Download and unpack mediainfo - curl -O https://mediaarea.net/download/binary/mediainfo/0.7.90/MediaInfo_CLI_0.7.90_Windows_i386.zip - mkdir MediaInfo - 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip - - cp MediaInfo\\MediaInfo.exe openlp\\MediaInfo.exe + - cp MediaInfo\\MediaInfo.exe openlp-branch\\MediaInfo.exe + + +build: off + +test_script: + - cd openlp-branch + - "%PYTHON%\\python.exe -m nose -v tests" + # Go back to the user root folder + - cd.. + +after_test: + # This is where we create a package using PyInstaller + # First get PyInstaller + - curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip + - 7z x develop.zip + # Install PyInstaller dependencies + - "%PYTHON%\\python.exe -m pip install future" + # Download and install Inno Setup - used for packaging + - curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe + - is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP- + # Download and unpack portable-bundle + - curl -O https://get.openlp.org/win-sdk/portable-setup.7z + - 7z x portable-setup.7z # Disabled portable installers - can't figure out how to make them silent # - curl -L -O http://downloads.sourceforge.net/project/portableapps/PortableApps.com%20Installer/PortableApps.comInstaller_3.4.4.paf.exe # - PortableApps.comInstaller_3.4.4.paf.exe /S @@ -46,18 +61,11 @@ install: # - PortableApps.comLauncher_2.2.1.paf.exe /S # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe # - NSISPortable_3.0_English.paf.exe /S - - -build: off - -test_script: - - cd openlp - - "%PYTHON%\\python.exe -m nose -v tests" - -after_test: - # This is where we create a package using PyInstaller - # First get PyInstaller - - curl -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip - - 7z x develop.zip + # Get the packaging code + - bzr checkout --lightweight lp:~tomasgroth/openlp/packaging-appveyor packaging # Build the bundle - - "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp" + - cd packaging + - "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch" + +artifacts: + - path: openlp-branch\dist\*.exe From 19a450fab5e7556c609d7909d4d01c9b5dd8bbe1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 1 Dec 2016 09:10:51 +0100 Subject: [PATCH 16/40] Use the standard packaging repo. --- scripts/appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 4784ed50d..43647bf71 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -62,7 +62,7 @@ after_test: # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe # - NSISPortable_3.0_English.paf.exe /S # Get the packaging code - - bzr checkout --lightweight lp:~tomasgroth/openlp/packaging-appveyor packaging + - bzr checkout --lightweight lp:openlp/packaging packaging # Build the bundle - cd packaging - "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch" From 23a4fcb665f7a559dad46b377bd8f687d896d573 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 9 Dec 2016 22:22:47 +0100 Subject: [PATCH 17/40] rewrite the appveyor.yml --- scripts/appveyor.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 43647bf71..e60473cc5 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -37,17 +37,19 @@ build: off test_script: - cd openlp-branch - - "%PYTHON%\\python.exe -m nose -v tests" + #- "%PYTHON%\\python.exe -m nose -v tests" # Go back to the user root folder - cd.. after_test: # This is where we create a package using PyInstaller # First get PyInstaller - - curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip - - 7z x develop.zip + #- curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip + - curl -L -O https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip + - 7z x PyInstaller-3.2.zip + #- mv pyinstaller-develop PyInstaller-3.2 # Install PyInstaller dependencies - - "%PYTHON%\\python.exe -m pip install future" + - "%PYTHON%\\python.exe -m pip install future pefile" # Download and install Inno Setup - used for packaging - curl -L -O http://www.jrsoftware.org/download.php/is-unicode.exe - is-unicode.exe /VERYSILENT /SUPPRESSMSGBOXES /SP- @@ -62,10 +64,28 @@ after_test: # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe # - NSISPortable_3.0_English.paf.exe /S # Get the packaging code - - bzr checkout --lightweight lp:openlp/packaging packaging + #- bzr checkout --lightweight lp:openlp/packaging packaging + - bzr checkout --lightweight lp:~raoul-snyman/openlp/pyinstaller-change packaging + #- curl -L http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -o packaging.tar.gz + #- 7z e packaging.tar.gz + #- 7z x packaging.tar + #- mv ~openlp-core/openlp/packaging packaging + # If this is trunk we should also build the manual + # Download and install HTML Help Workshop + #- curl -L "http://go.microsoft.com/fwlink/p/?linkid=14188" -o htmlhelp.exe + #- htmlhelp.exe /Q + # Install sphinx + - "%PYTHON%\\python.exe -m pip install sphinx" + # Get the documentation code + #- bzr checkout --lightweight lp:openlp/documentation documentation + - curl -L http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball -o documentation.tar.gz + - 7z e documentation.tar.gz + - 7z x documentation.tar + - mv ~openlp-core/openlp/documentation documentation # Build the bundle - cd packaging - - "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch" + #- "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation" + - "%PYTHON%\\python.exe builders/windows-builder.py -v --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable" artifacts: - path: openlp-branch\dist\*.exe From 79934596ac47c5e90a89b9eb7f963db1e2d3f351 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 00:14:45 +0200 Subject: [PATCH 18/40] Fix the problem with the Library/Media Manager icons not showing up. --- openlp/core/ui/lib/mediadockmanager.py | 2 +- openlp/core/ui/mainwindow.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openlp/core/ui/lib/mediadockmanager.py b/openlp/core/ui/lib/mediadockmanager.py index ad786b3a0..1a7676465 100644 --- a/openlp/core/ui/lib/mediadockmanager.py +++ b/openlp/core/ui/lib/mediadockmanager.py @@ -54,7 +54,7 @@ class MediaDockManager(object): match = True break if not match: - self.media_dock.addItem(media_item, visible_title['title']) + self.media_dock.addItem(media_item, media_item.plugin.icon, visible_title['title']) def remove_dock(self, media_item): """ diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 92b29d16f..55153e29d 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -52,19 +52,18 @@ from openlp.core.ui.lib.mediadockmanager import MediaDockManager log = logging.getLogger(__name__) MEDIA_MANAGER_STYLE = """ -QToolBox { - padding-bottom: 2px; -} -QToolBox::tab { +::tab { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(button), stop: 1.0 palette(mid)); border: 1px solid palette(mid); - border-radius: 3px; + margin-top: 0; + margin-bottom: 0; + text-align: left; } -QToolBox::tab:selected { +::tab:selected { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 1.0 palette(button)); - border: 1px solid palette(mid); + border: 1px solid palette(highlight); font-weight: bold; } """ From 19a00b44e06f4f771910f3bfc58dabb92aabfed4 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 00:16:23 +0200 Subject: [PATCH 19/40] Update the version number to 2.5.0 just for any dev builds that might happen --- openlp/.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/.version b/openlp/.version index 6b4950e3d..437459cd9 100644 --- a/openlp/.version +++ b/openlp/.version @@ -1 +1 @@ -2.4 +2.5.0 From 97dfafba4e23747e64b76f3c2bf2c134b8e0c860 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 08:04:27 +0200 Subject: [PATCH 20/40] Make the tabs in the media library prettier --- openlp/core/ui/mainwindow.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 55153e29d..fb53bf126 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -55,14 +55,13 @@ MEDIA_MANAGER_STYLE = """ ::tab { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(button), stop: 1.0 palette(mid)); - border: 1px solid palette(mid); + border: 0; + border-radius: 2px; margin-top: 0; margin-bottom: 0; text-align: left; } ::tab:selected { - background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 palette(light), stop: 1.0 palette(button)); border: 1px solid palette(highlight); font-weight: bold; } From c818c3ce8c5fb7aa4f4fbbb6b6aea1cf406aa1e4 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 16:55:11 +0200 Subject: [PATCH 21/40] Added a test --- .../openlp_core_ui/test_aboutform.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_aboutform.py b/tests/functional/openlp_core_ui/test_aboutform.py index 612c6b887..381ed83c8 100644 --- a/tests/functional/openlp_core_ui/test_aboutform.py +++ b/tests/functional/openlp_core_ui/test_aboutform.py @@ -32,16 +32,30 @@ from tests.helpers.testmixin import TestMixin class TestFirstTimeForm(TestCase, TestMixin): - def test_on_volunteer_button_clicked(self): + @patch('openlp.core.ui.aboutform.webbrowser') + def test_on_volunteer_button_clicked(self, mocked_webbrowser): """ Test that clicking on the "Volunteer" button opens a web page. """ # GIVEN: A new About dialog and a mocked out webbrowser module - with patch('openlp.core.ui.aboutform.webbrowser') as mocked_webbrowser: - about_form = AboutForm(None) + about_form = AboutForm(None) - # WHEN: The "Volunteer" button is "clicked" - about_form.on_volunteer_button_clicked() + # WHEN: The "Volunteer" button is "clicked" + about_form.on_volunteer_button_clicked() - # THEN: A web browser is opened - mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute') + # THEN: A web browser is opened + mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute') + + @patch('openlp.core.ui.aboutform.get_application_version') + def test_about_form_build_number(self, mocked_get_application_version): + """ + Test that the build number is added to the about form + """ + # GIVEN: A mocked out get_application_version function + mocked_get_application_version.return_value = {'version': '3.1.5', 'build': '3000'} + + # WHEN: The about form is created + about_form = AboutForm(None) + + # THEN: The build number should be in the text + assert about_form.about_text_edit.plainText().split('\n')[0] == '' From f17204aa19d3937929c52632529cc2c1689e2bf3 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 21:43:27 +0200 Subject: [PATCH 22/40] Fix a problem with the test --- tests/functional/openlp_core_ui/test_aboutform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_ui/test_aboutform.py b/tests/functional/openlp_core_ui/test_aboutform.py index 381ed83c8..60c4c2c68 100644 --- a/tests/functional/openlp_core_ui/test_aboutform.py +++ b/tests/functional/openlp_core_ui/test_aboutform.py @@ -58,4 +58,4 @@ class TestFirstTimeForm(TestCase, TestMixin): about_form = AboutForm(None) # THEN: The build number should be in the text - assert about_form.about_text_edit.plainText().split('\n')[0] == '' + assert about_form.about_text_edit.toPlainText().split('\n')[0] == '' From ddb92c3cd5d32997f32e89c1f09a31cd52527c03 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Dec 2016 21:50:34 +0200 Subject: [PATCH 23/40] Fix a problem with the test --- tests/functional/openlp_core_ui/test_aboutform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_ui/test_aboutform.py b/tests/functional/openlp_core_ui/test_aboutform.py index 60c4c2c68..47a685f9d 100644 --- a/tests/functional/openlp_core_ui/test_aboutform.py +++ b/tests/functional/openlp_core_ui/test_aboutform.py @@ -58,4 +58,4 @@ class TestFirstTimeForm(TestCase, TestMixin): about_form = AboutForm(None) # THEN: The build number should be in the text - assert about_form.about_text_edit.toPlainText().split('\n')[0] == '' + assert 'OpenLP 3.1.5 build 3000' in about_form.about_text_edit.toPlainText() From 3639785151ef57223c991766925ef66c36fba15f Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 15 Dec 2016 16:11:42 +0200 Subject: [PATCH 24/40] Fix bug #1642684 by rather just setting the edit text to a blank string Fixes: https://launchpad.net/bugs/1642684 --- openlp/plugins/songs/forms/editsongform.py | 19 ++++++------ .../openlp_plugins/songs/test_editsongform.py | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index a17c9fb5f..271dadce7 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -118,13 +118,13 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): objects = self.manager.get_all_objects(cls) objects.sort(key=get_key) combo.clear() - combo.addItem('') for obj in objects: row = combo.count() combo.addItem(obj.name) cache.append(obj.name) combo.setItemData(row, obj.id) set_case_insensitive_completer(cache, combo) + combo.setEditText('') def _add_author_to_list(self, author, author_type): """ @@ -360,7 +360,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): authors = self.manager.get_all_objects(Author) authors.sort(key=get_author_key) self.authors_combo_box.clear() - self.authors_combo_box.addItem('') self.authors = [] for author in authors: row = self.authors_combo_box.count() @@ -368,6 +367,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.authors_combo_box.setItemData(row, author.id) self.authors.append(author.display_name) set_case_insensitive_completer(self.authors, self.authors_combo_box) + self.authors_combo_box.setEditText('') # Types self.author_types_combo_box.clear() @@ -398,11 +398,11 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): return get_natural_key(theme) self.theme_combo_box.clear() - self.theme_combo_box.addItem('') self.themes = theme_list self.themes.sort(key=get_theme_key) self.theme_combo_box.addItems(theme_list) set_case_insensitive_completer(self.themes, self.theme_combo_box) + self.theme_combo_box.setEditText('') def load_media_files(self): """ @@ -442,7 +442,6 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.load_songbooks() self.load_media_files() self.theme_combo_box.setEditText('') - self.theme_combo_box.setCurrentIndex(0) # it's a new song to preview is not possible self.preview_button.setVisible(False) @@ -591,7 +590,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.manager.save_object(author) self._add_author_to_list(author, author_type) self.load_authors() - self.authors_combo_box.setCurrentIndex(0) + self.authors_combo_box.setEditText('') else: return elif item > 0: @@ -602,7 +601,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): message=translate('SongsPlugin.EditSongForm', 'This author is already in the list.')) else: self._add_author_to_list(author, author_type) - self.authors_combo_box.setCurrentIndex(0) + self.authors_combo_box.setEditText('') else: QtWidgets.QMessageBox.warning( self, UiStrings().NISs, @@ -666,7 +665,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_item.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_item) self.load_topics() - self.topics_combo_box.setCurrentIndex(0) + self.topics_combo_box.setEditText('') else: return elif item > 0: @@ -679,7 +678,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): topic_item = QtWidgets.QListWidgetItem(str(topic.name)) topic_item.setData(QtCore.Qt.UserRole, topic.id) self.topics_list_view.addItem(topic_item) - self.topics_combo_box.setCurrentIndex(0) + self.topics_combo_box.setEditText('') else: QtWidgets.QMessageBox.warning( self, UiStrings().NISs, @@ -709,7 +708,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): self.manager.save_object(songbook) self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) self.load_songbooks() - self.songbooks_combo_box.setCurrentIndex(0) + self.songbooks_combo_box.setEditText('') self.songbook_entry_edit.clear() else: return @@ -721,7 +720,7 @@ class EditSongForm(QtWidgets.QDialog, Ui_EditSongDialog, RegistryProperties): message=translate('SongsPlugin.EditSongForm', 'This Songbook is already in the list.')) else: self.add_songbook_entry_to_list(songbook.id, songbook.name, self.songbook_entry_edit.text()) - self.songbooks_combo_box.setCurrentIndex(0) + self.songbooks_combo_box.setEditText('') self.songbook_entry_edit.clear() else: QtWidgets.QMessageBox.warning( diff --git a/tests/functional/openlp_plugins/songs/test_editsongform.py b/tests/functional/openlp_plugins/songs/test_editsongform.py index 184c59717..ba53fa525 100644 --- a/tests/functional/openlp_plugins/songs/test_editsongform.py +++ b/tests/functional/openlp_plugins/songs/test_editsongform.py @@ -76,3 +76,34 @@ class TestEditSongForm(TestCase, TestMixin): # THEN they should be valid self.assertTrue(valid, "The tags list should be valid") + + @patch('openlp.plugins.songs.forms.editsongform.set_case_insensitive_completer') + def test_load_objects(self, mocked_set_case_insensitive_completer): + """ + Test the _load_objects() method + """ + # GIVEN: A song edit form and some mocked stuff + mocked_class = MagicMock() + mocked_class.name = 'Author' + mocked_combo = MagicMock() + mocked_combo.count.return_value = 0 + mocked_cache = MagicMock() + mocked_object = MagicMock() + mocked_object.name = 'Charles' + mocked_object.id = 1 + mocked_manager = MagicMock() + mocked_manager.get_all_objects.return_value = [mocked_object] + self.edit_song_form.manager = mocked_manager + + # WHEN: _load_objects() is called + self.edit_song_form._load_objects(mocked_class, mocked_combo, mocked_cache) + + # THEN: All the correct methods should have been called + self.edit_song_form.manager.get_all_objects.assert_called_once_with(mocked_class) + mocked_combo.clear.assert_called_once_with() + mocked_combo.count.assert_called_once_with() + mocked_combo.addItem.assert_called_once_with('Charles') + mocked_cache.append.assert_called_once_with('Charles') + mocked_combo.setItemData.assert_called_once_with(0, 1) + mocked_set_case_insensitive_completer.assert_called_once_with(mocked_cache, mocked_combo) + mocked_combo.setEditText.assert_called_once_with('') From dcfd0a83a6e7a0dd183987bf1b536a1419063435 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 15 Dec 2016 17:33:03 +0100 Subject: [PATCH 25/40] Updates to the appveyor. Only build docs if building trunk. --- scripts/appveyor-webhook.py | 10 +++++++--- scripts/appveyor.yml | 38 ++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/scripts/appveyor-webhook.py b/scripts/appveyor-webhook.py index 573aa1045..45438645d 100755 --- a/scripts/appveyor-webhook.py +++ b/scripts/appveyor-webhook.py @@ -42,7 +42,7 @@ webhook_element = \ { 'commit': { 'author': { - 'email': 'open@contributer', + 'email': 'contributer@openlp', 'name': 'OpenLP Contributor' }, 'id': None, @@ -92,6 +92,10 @@ def get_yml(branch): yml_text = f.read() f.close() yml_text = yml_text.replace('BRANCHNAME', branch) + if 'openlp-core/openlp/trunk' in branch: + yml_text = yml_text.replace('BUILD_DOCS', '$TRUE') + else: + yml_text = yml_text.replace('BUILD_DOCS', '$FALSE') return yml_text @@ -115,8 +119,6 @@ def get_appveyor_build_url(branch): """ Get the url of the build. """ - # Wait 10 seconds to make sure the hook has been triggered - time.sleep(10) responce = urllib.request.urlopen(appveyor_api_url) json_str = responce.read().decode('utf-8') build_json = json.loads(json_str) @@ -130,4 +132,6 @@ else: webhook_url = sys.argv[1] branch = sys.argv[2] hook(webhook_url, get_yml(branch)) + # Wait 5 seconds to make sure the hook has been triggered + time.sleep(5) get_appveyor_build_url(branch) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index e60473cc5..bdc23f303 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -44,10 +44,8 @@ test_script: after_test: # This is where we create a package using PyInstaller # First get PyInstaller - #- curl -L -O https://github.com/pyinstaller/pyinstaller/archive/develop.zip - curl -L -O https://github.com/pyinstaller/pyinstaller/releases/download/v3.2/PyInstaller-3.2.zip - 7z x PyInstaller-3.2.zip - #- mv pyinstaller-develop PyInstaller-3.2 # Install PyInstaller dependencies - "%PYTHON%\\python.exe -m pip install future pefile" # Download and install Inno Setup - used for packaging @@ -64,28 +62,38 @@ after_test: # - curl -L -O http://downloads.sourceforge.net/project/portableapps/NSIS%20Portable/NSISPortable_3.0_English.paf.exe # - NSISPortable_3.0_English.paf.exe /S # Get the packaging code - #- bzr checkout --lightweight lp:openlp/packaging packaging - - bzr checkout --lightweight lp:~raoul-snyman/openlp/pyinstaller-change packaging - #- curl -L http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -o packaging.tar.gz - #- 7z e packaging.tar.gz - #- 7z x packaging.tar - #- mv ~openlp-core/openlp/packaging packaging + - curl -L http://bazaar.launchpad.net/~openlp-core/openlp/packaging/tarball -o packaging.tar.gz + - 7z e packaging.tar.gz + - 7z x packaging.tar + - mv ~openlp-core/openlp/packaging packaging # If this is trunk we should also build the manual # Download and install HTML Help Workshop #- curl -L "http://go.microsoft.com/fwlink/p/?linkid=14188" -o htmlhelp.exe #- htmlhelp.exe /Q # Install sphinx - - "%PYTHON%\\python.exe -m pip install sphinx" + - ps: >- + If (BUILD_DOCS) { + &"$env:PYTHON\python.exe" -m pip install sphinx + Invoke-WebRequest -Uri "http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball" -OutFile documentation.tar.gz + 7z e documentation.tar.gz + 7z x documentation.tar + mv ~openlp-core/openlp/documentation documentation + cd packaging + &"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable + } else { + cd packaging + &"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch --portable + } # Get the documentation code #- bzr checkout --lightweight lp:openlp/documentation documentation - - curl -L http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball -o documentation.tar.gz - - 7z e documentation.tar.gz - - 7z x documentation.tar - - mv ~openlp-core/openlp/documentation documentation + #- curl -L http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball -o documentation.tar.gz + #- 7z e documentation.tar.gz + #- 7z x documentation.tar + #- mv ~openlp-core/openlp/documentation documentation # Build the bundle - - cd packaging + #- cd packaging #- "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation" - - "%PYTHON%\\python.exe builders/windows-builder.py -v --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable" + #- "%PYTHON%\\python.exe builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable" artifacts: - path: openlp-branch\dist\*.exe From 47ab1ce1a7de7f43e8515357514897aeb0940942 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 15 Dec 2016 19:45:46 +0200 Subject: [PATCH 26/40] Hide the splash screen when the backup dialog shows and when the exception form shows --- openlp/core/__init__.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index cb298e11b..ad06f3629 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -129,21 +129,21 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) - show_splash = Settings().value('core/show splash') - if show_splash: + can_show_splash = Settings().value('core/show splash') + if can_show_splash: self.splash = SplashScreen() self.splash.show() # make sure Qt really display the splash screen self.processEvents() # Check if OpenLP has been upgrade and if a backup of data should be created - self.backup_on_upgrade(has_run_wizard) + self.backup_on_upgrade(has_run_wizard, can_show_splash) # start the main app window self.main_window = MainWindow() Registry().execute('bootstrap_initialise') Registry().execute('bootstrap_post_set_up') Registry().initialise = False self.main_window.show() - if show_splash: + if can_show_splash: # now kill the splashscreen self.splash.finish(self.main_window) log.debug('Splashscreen closed') @@ -224,13 +224,20 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): self.exception_form = ExceptionForm() self.exception_form.exception_text_edit.setPlainText(''.join(format_exception(exc_type, value, traceback))) self.set_normal_cursor() + is_splash_visible = False + if hasattr(self, 'splash') and self.splash.isVisible(): + is_splash_visible = True + self.splash.hide() self.exception_form.exec() + if is_splash_visible: + self.splash.show() - def backup_on_upgrade(self, has_run_wizard): + def backup_on_upgrade(self, has_run_wizard, can_show_splash): """ Check if OpenLP has been upgraded, and ask if a backup of data should be made :param has_run_wizard: OpenLP has been run before + :param can_show_splash: Should OpenLP show the splash screen """ data_version = Settings().value('core/application version') openlp_version = get_application_version()['version'] @@ -239,6 +246,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): Settings().setValue('core/application version', openlp_version) # If data_version is different from the current version ask if we should backup the data folder elif data_version != openlp_version: + if self.splash.isVisible(): + self.splash.hide() if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'), translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n' 'a backup of the old data folder?'), @@ -261,6 +270,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication): # Update the version in the settings Settings().setValue('core/application version', openlp_version) + if can_show_splash: + self.splash.show() def process_events(self): """ From acb8bc2ba1864fbbe6ddfe27c65b52d911ab925f Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 15 Dec 2016 19:24:11 +0100 Subject: [PATCH 27/40] clean up appveyor.yml --- scripts/appveyor.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index bdc23f303..89621d150 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -3,7 +3,6 @@ version: OpenLP-win-ci-b{build} init: - choco install -y --force bzr - set PATH=C:\Program Files (x86)\Bazaar;%PATH% - - bzr --version clone_script: - bzr checkout --lightweight BRANCHNAME openlp-branch @@ -32,7 +31,6 @@ install: - 7z x -oMediaInfo MediaInfo_CLI_0.7.90_Windows_i386.zip - cp MediaInfo\\MediaInfo.exe openlp-branch\\MediaInfo.exe - build: off test_script: @@ -67,10 +65,6 @@ after_test: - 7z x packaging.tar - mv ~openlp-core/openlp/packaging packaging # If this is trunk we should also build the manual - # Download and install HTML Help Workshop - #- curl -L "http://go.microsoft.com/fwlink/p/?linkid=14188" -o htmlhelp.exe - #- htmlhelp.exe /Q - # Install sphinx - ps: >- If (BUILD_DOCS) { &"$env:PYTHON\python.exe" -m pip install sphinx @@ -84,16 +78,6 @@ after_test: cd packaging &"$env:PYTHON\python.exe" builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch --portable } - # Get the documentation code - #- bzr checkout --lightweight lp:openlp/documentation documentation - #- curl -L http://bazaar.launchpad.net/~openlp-core/openlp/documentation/tarball -o documentation.tar.gz - #- 7z e documentation.tar.gz - #- 7z x documentation.tar - #- mv ~openlp-core/openlp/documentation documentation - # Build the bundle - #- cd packaging - #- "%PYTHON%\\python.exe windows/windows-builder.py -v -u -t -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation" - #- "%PYTHON%\\python.exe builders/windows-builder.py --skip-update --skip-translations -c windows/config-appveyor.ini -b ../openlp-branch -d ../documentation --portable" artifacts: - path: openlp-branch\dist\*.exe From 936e6c8816034c897feba9456430a0567aeb94dc Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 15 Dec 2016 20:26:18 +0100 Subject: [PATCH 28/40] enable running tests --- scripts/appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/appveyor.yml b/scripts/appveyor.yml index 89621d150..27b4cfc98 100644 --- a/scripts/appveyor.yml +++ b/scripts/appveyor.yml @@ -35,7 +35,8 @@ build: off test_script: - cd openlp-branch - #- "%PYTHON%\\python.exe -m nose -v tests" + # Run the tests + - "%PYTHON%\\python.exe -m nose -v tests" # Go back to the user root folder - cd.. From 95eb290226ffe2d26a063502d07df1faf531398b Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 15 Dec 2016 22:36:04 +0200 Subject: [PATCH 29/40] Make the tab style affect only the media library tabs, not everything else too --- openlp/core/ui/mainwindow.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index ab92914a5..ec585790c 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -52,7 +52,7 @@ from openlp.core.ui.lib.mediadockmanager import MediaDockManager log = logging.getLogger(__name__) MEDIA_MANAGER_STYLE = """ -::tab { +::tab#media_tool_box { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(button), stop: 1.0 palette(mid)); border: 0; @@ -61,10 +61,8 @@ MEDIA_MANAGER_STYLE = """ margin-bottom: 0; text-align: left; } -::tab:selected { - border: 1px solid palette(highlight); - font-weight: bold; -} +/* This is here to make the tabs on KDE with the Breeze theme work */ +::tab:selected {} """ PROGRESSBAR_STYLE = """ From c86d669346c832c0d9a049717f956e0b16f6af52 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 15 Dec 2016 23:29:29 +0200 Subject: [PATCH 30/40] Added a test --- .../openlp_core_ui/test_shortcutlistdialog.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/functional/openlp_core_ui/test_shortcutlistdialog.py diff --git a/tests/functional/openlp_core_ui/test_shortcutlistdialog.py b/tests/functional/openlp_core_ui/test_shortcutlistdialog.py new file mode 100644 index 000000000..2f39cb34a --- /dev/null +++ b/tests/functional/openlp_core_ui/test_shortcutlistdialog.py @@ -0,0 +1,60 @@ +# -*- 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.ui.shortcutlistdialog package. +""" +from PyQt5 import QtCore, QtGui, QtWidgets + +from openlp.core.ui.shortcutlistdialog import CaptureShortcutButton, ShortcutTreeWidget + +from tests.interfaces import MagicMock, patch + + +def test_key_press_event(): + """ + Test the keyPressEvent method + """ + # GIVEN: A checked button and a mocked event + button = CaptureShortcutButton() + button.setChecked(True) + mocked_event = MagicMock() + mocked_event.key.return_value = QtCore.Qt.Key_Space + + # WHEN: keyPressEvent is called with an event that should be ignored + button.keyPressEvent(mocked_event) + + # THEN: The ignore() method on the event should have been called + mocked_event.ignore.assert_called_once_with() + + +def test_keyboard_search(): + """ + Test the keyboardSearch method of the ShortcutTreeWidget + """ + # GIVEN: A ShortcutTreeWidget + widget = ShortcutTreeWidget() + + # WHEN: keyboardSearch() is called + widget.keyboardSearch('') + + # THEN: Nothing happens + assert True From 77e048f6de2982f71ee039da9b9fe3fafdcfc910 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 16 Dec 2016 00:01:15 +0200 Subject: [PATCH 31/40] Fix the tests --- tests/functional/test_init.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_init.py b/tests/functional/test_init.py index 825de57f3..504489416 100644 --- a/tests/functional/test_init.py +++ b/tests/functional/test_init.py @@ -102,7 +102,7 @@ class TestInit(TestCase, TestMixin): mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created - self.openlp.backup_on_upgrade(old_install) + self.openlp.backup_on_upgrade(old_install, False) # THEN: It should not ask if we want to create a backup self.assertEqual(Settings().value('core/application version'), '2.2.0', 'Version should be the same!') @@ -120,14 +120,18 @@ class TestInit(TestCase, TestMixin): 'build': 'bzr000' } Settings().setValue('core/application version', '2.0.5') + self.openlp.splash = MagicMock() + self.openlp.splash.isVisible.return_value = True with patch('openlp.core.get_application_version') as mocked_get_application_version,\ patch('openlp.core.QtWidgets.QMessageBox.question') as mocked_question: mocked_get_application_version.return_value = MOCKED_VERSION mocked_question.return_value = QtWidgets.QMessageBox.No # WHEN: We check if a backup should be created - self.openlp.backup_on_upgrade(old_install) + self.openlp.backup_on_upgrade(old_install, True) # THEN: It should ask if we want to create a backup self.assertEqual(Settings().value('core/application version'), '2.2.0', 'Version should be upgraded!') self.assertEqual(mocked_question.call_count, 1, 'A question should have been asked!') + self.openlp.splash.hide.assert_called_once_with() + self.openlp.splash.show.assert_called_once_with() From 4008ed008feabd4ab6c8d785bbf39f58c47cb79b Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 20 Dec 2016 21:20:54 +0000 Subject: [PATCH 32/40] Move url size --- openlp/core/lib/webpagereader.py | 182 -------------- openlp/core/ui/firsttimeform.py | 28 +-- openlp/plugins/bibles/lib/importers/http.py | 2 +- .../openlp_core_lib/test_webpagereader.py | 229 ------------------ .../openlp_core_ui/test_first_time.py | 2 +- 5 files changed, 6 insertions(+), 437 deletions(-) delete mode 100644 openlp/core/lib/webpagereader.py delete mode 100644 tests/functional/openlp_core_lib/test_webpagereader.py diff --git a/openlp/core/lib/webpagereader.py b/openlp/core/lib/webpagereader.py deleted file mode 100644 index 52c98bbaf..000000000 --- a/openlp/core/lib/webpagereader.py +++ /dev/null @@ -1,182 +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 # -############################################################################### -""" -The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. -""" -import logging -import socket -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from http.client import HTTPException -from random import randint - -from openlp.core.common import Registry - -log = logging.getLogger(__name__ + '.__init__') - -USER_AGENTS = { - 'win32': [ - 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', - 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36' - ], - 'darwin': [ - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) ' - 'Chrome/26.0.1410.43 Safari/537.31', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) ' - 'Chrome/20.0.1132.57 Safari/536.11', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) ' - 'Chrome/20.0.1132.47 Safari/536.11', - ], - 'linux2': [ - 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 ' - 'Chrome/25.0.1364.160 Safari/537.22', - 'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 ' - 'Safari/537.11', - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27' - ], - 'default': [ - 'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0' - ] -} -CONNECTION_TIMEOUT = 30 -CONNECTION_RETRIES = 2 - - -class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): - """ - Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 - (Redirecting to urls with special chars) - """ - def redirect_request(self, req, fp, code, msg, headers, new_url): - # - """ - Test if the new_url can be decoded to ascii - - :param req: - :param fp: - :param code: - :param msg: - :param headers: - :param new_url: - :return: - """ - try: - new_url.encode('latin1').decode('ascii') - fixed_url = new_url - except Exception: - # The url could not be decoded to ascii, so we do some url encoding - fixed_url = urllib.parse.quote(new_url.encode('latin1').decode('utf-8', 'replace'), safe='/:') - return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) - - -def _get_user_agent(): - """ - Return a user agent customised for the platform the user is on. - """ - browser_list = USER_AGENTS.get(sys.platform, None) - if not browser_list: - browser_list = USER_AGENTS['default'] - random_index = randint(0, len(browser_list) - 1) - return browser_list[random_index] - - -def get_web_page(url, header=None, update_openlp=False): - """ - Attempts to download the webpage at url and returns that page or None. - - :param url: The URL to be downloaded. - :param header: An optional HTTP header to pass in the request to the web server. - :param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded. - Defaults to False. - """ - # TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a - # proxy_handler, build into an opener and install the opener into urllib2. - # http://docs.python.org/library/urllib2.html - if not url: - return None - # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 - opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) - urllib.request.install_opener(opener) - req = urllib.request.Request(url) - if not header or header[0].lower() != 'user-agent': - user_agent = _get_user_agent() - req.add_header('User-Agent', user_agent) - if header: - req.add_header(header[0], header[1]) - log.debug('Downloading URL = %s' % url) - retries = 0 - while retries <= CONNECTION_RETRIES: - retries += 1 - time.sleep(0.1) - try: - page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT) - log.debug('Downloaded page {text}'.format(text=page.geturl())) - break - except urllib.error.URLError as err: - log.exception('URLError on {text}'.format(text=url)) - log.exception('URLError: {text}'.format(text=err.reason)) - page = None - if retries > CONNECTION_RETRIES: - raise - except socket.timeout: - log.exception('Socket timeout: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except socket.gaierror: - log.exception('Socket gaierror: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except ConnectionRefusedError: - log.exception('ConnectionRefused: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - break - except ConnectionError: - log.exception('Connection error: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except HTTPException: - log.exception('HTTPException error: {text}'.format(text=url)) - page = None - if retries > CONNECTION_RETRIES: - raise - except: - # Don't know what's happening, so reraise the original - raise - if update_openlp: - Registry().get('application').process_events() - if not page: - log.exception('{text} could not be downloaded'.format(text=url)) - return None - log.debug(page) - return page - - -__all__ = ['get_web_page'] diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 974ef90db..b59f31211 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.lib.webpagereader import get_web_page, CONNECTION_RETRIES, CONNECTION_TIMEOUT +from openlp.core.common.httputils import get_web_page, get_url_file_size, CONNECTION_RETRIES, CONNECTION_TIMEOUT from .firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) @@ -455,26 +455,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): if item: item.setIcon(build_icon(os.path.join(gettempdir(), 'openlp', screenshot))) - def _get_file_size(self, url): - """ - Get the size of a file. - - :param url: The URL of the file we want to download. - """ - retries = 0 - while True: - try: - site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - meta = site.info() - return int(meta.get("Content-Length")) - except urllib.error.URLError: - if retries > CONNECTION_RETRIES: - raise - else: - retries += 1 - time.sleep(0.1) - continue - def _download_progress(self, count, block_size): """ Calculate and display the download progress. @@ -510,7 +490,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = self.songs_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename, sha256 = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.songs_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.songs_url, name=filename)) self.max_progress += size # Loop through the Bibles list and increase for each selected item iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -519,7 +499,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = iterator.value() if item.parent() and item.checkState(0) == QtCore.Qt.Checked: filename, sha256 = item.data(0, QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.bibles_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.bibles_url, name=filename)) self.max_progress += size iterator += 1 # Loop through the themes list and increase for each selected item @@ -528,7 +508,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): item = self.themes_list_widget.item(i) if item.checkState() == QtCore.Qt.Checked: filename, sha256 = item.data(QtCore.Qt.UserRole) - size = self._get_file_size('{path}{name}'.format(path=self.themes_url, name=filename)) + size = get_url_file_size('{path}{name}'.format(path=self.themes_url, name=filename)) self.max_progress += size except urllib.error.URLError: trace_error_handler(log) diff --git a/openlp/plugins/bibles/lib/importers/http.py b/openlp/plugins/bibles/lib/importers/http.py index d41187d93..071ab0119 100644 --- a/openlp/plugins/bibles/lib/importers/http.py +++ b/openlp/plugins/bibles/lib/importers/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.lib.webpagereader import get_web_page +from openlp.core.common.httputils import get_web_page from openlp.plugins.bibles.lib import SearchResults from openlp.plugins.bibles.lib.bibleimport import BibleImport from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB, Book diff --git a/tests/functional/openlp_core_lib/test_webpagereader.py b/tests/functional/openlp_core_lib/test_webpagereader.py deleted file mode 100644 index 6e33fca51..000000000 --- a/tests/functional/openlp_core_lib/test_webpagereader.py +++ /dev/null @@ -1,229 +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 # -############################################################################### -""" -Functional tests to test the AppLocation class and related methods. -""" -from unittest import TestCase - -from openlp.core.lib.webpagereader import _get_user_agent, get_web_page - -from tests.functional import MagicMock, patch - - -class TestUtils(TestCase): - """ - A test suite to test out various methods around the AppLocation class. - """ - def test_get_user_agent_linux(self): - """ - Test that getting a user agent on Linux returns a user agent suitable for Linux - """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: - - # GIVEN: The system is Linux - mocked_sys.platform = 'linux2' - - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() - - # THEN: The user agent is a Linux (or ChromeOS) user agent - result = 'Linux' in user_agent or 'CrOS' in user_agent - self.assertTrue(result, 'The user agent should be a valid Linux user agent') - - def test_get_user_agent_windows(self): - """ - Test that getting a user agent on Windows returns a user agent suitable for Windows - """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: - - # GIVEN: The system is Linux - mocked_sys.platform = 'win32' - - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() - - # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent') - - def test_get_user_agent_macos(self): - """ - Test that getting a user agent on OS X returns a user agent suitable for OS X - """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: - - # GIVEN: The system is Linux - mocked_sys.platform = 'darwin' - - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() - - # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent') - - def test_get_user_agent_default(self): - """ - Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent - """ - with patch('openlp.core.lib.webpagereader.sys') as mocked_sys: - - # GIVEN: The system is Linux - mocked_sys.platform = 'freebsd' - - # WHEN: We call _get_user_agent() - user_agent = _get_user_agent() - - # THEN: The user agent is a Linux (or ChromeOS) user agent - self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent') - - def test_get_web_page_no_url(self): - """ - Test that sending a URL of None to the get_web_page method returns None - """ - # GIVEN: A None url - test_url = None - - # WHEN: We try to get the test URL - result = get_web_page(test_url) - - # THEN: None should be returned - self.assertIsNone(result, 'The return value of get_web_page should be None') - - def test_get_web_page(self): - """ - Test that the get_web_page method works correctly - """ - 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 - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - fake_url = 'this://is.a.fake/url' - - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url) - - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called') - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') - - def test_get_web_page_with_header(self): - """ - Test that adding a header to the call to get_web_page() adds the header to the request - """ - 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() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - fake_url = 'this://is.a.fake/url' - fake_header = ('Fake-Header', 'fake value') - - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, header=fake_header) - - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1]) - self.assertEqual(2, mocked_request_object.add_header.call_count, - 'There should only be 2 calls to add_header') - mock_get_user_agent.assert_called_with() - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') - - def test_get_web_page_with_user_agent_in_headers(self): - """ - 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.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() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - fake_url = 'this://is.a.fake/url' - user_agent_header = ('User-Agent', 'OpenLP/2.2.0') - - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, header=user_agent_header) - - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1]) - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - self.assertEqual(0, mock_get_user_agent.call_count, '_get_user_agent should not have been called') - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') - - def test_get_web_page_update_openlp(self): - """ - Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() - """ - 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.webpagereader.Registry') as MockRegistry: - # GIVEN: Mocked out objects, a fake URL - mocked_request_object = MagicMock() - MockRequest.return_value = mocked_request_object - mocked_page_object = MagicMock() - mock_urlopen.return_value = mocked_page_object - mock_get_user_agent.return_value = 'user_agent' - mocked_registry_object = MagicMock() - mocked_application_object = MagicMock() - mocked_registry_object.get.return_value = mocked_application_object - MockRegistry.return_value = mocked_registry_object - fake_url = 'this://is.a.fake/url' - - # WHEN: The get_web_page() method is called - returned_page = get_web_page(fake_url, update_openlp=True) - - # THEN: The correct methods are called with the correct arguments and a web page is returned - MockRequest.assert_called_with(fake_url) - mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') - self.assertEqual(1, mocked_request_object.add_header.call_count, - 'There should only be 1 call to add_header') - mock_urlopen.assert_called_with(mocked_request_object, timeout=30) - mocked_page_object.geturl.assert_called_with() - mocked_registry_object.get.assert_called_with('application') - mocked_application_object.process_events.assert_called_with() - self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') diff --git a/tests/functional/openlp_core_ui/test_first_time.py b/tests/functional/openlp_core_ui/test_first_time.py index d8067dfbe..f23bf4db6 100644 --- a/tests/functional/openlp_core_ui/test_first_time.py +++ b/tests/functional/openlp_core_ui/test_first_time.py @@ -31,7 +31,7 @@ import urllib.parse from tests.functional import patch from tests.helpers.testmixin import TestMixin -from openlp.core.lib.webpagereader import CONNECTION_RETRIES, get_web_page +from openlp.core.common.httputils import CONNECTION_RETRIES, get_web_page class TestFirstTimeWizard(TestMixin, TestCase): From 95e70465de6737ddd4377b871874187bdb67c25f Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 20 Dec 2016 21:21:17 +0000 Subject: [PATCH 33/40] Add files --- openlp/core/common/httputils.py | 203 ++++++++++++++ .../openlp_core_common/test_httputils.py | 247 ++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 openlp/core/common/httputils.py create mode 100644 tests/functional/openlp_core_common/test_httputils.py diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py new file mode 100644 index 000000000..cab3ffb49 --- /dev/null +++ b/openlp/core/common/httputils.py @@ -0,0 +1,203 @@ +# -*- 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 # +############################################################################### +""" +The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. +""" +import logging +import socket +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from http.client import HTTPException +from random import randint + +from openlp.core.common import Registry + +log = logging.getLogger(__name__ + '.__init__') + +USER_AGENTS = { + 'win32': [ + 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36', + 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36' + ], + 'darwin': [ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) ' + 'Chrome/26.0.1410.43 Safari/537.31', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) ' + 'Chrome/20.0.1132.57 Safari/536.11', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) ' + 'Chrome/20.0.1132.47 Safari/536.11', + ], + 'linux2': [ + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 ' + 'Chrome/25.0.1364.160 Safari/537.22', + 'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 ' + 'Safari/537.11', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27' + ], + 'default': [ + 'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0' + ] +} +CONNECTION_TIMEOUT = 30 +CONNECTION_RETRIES = 2 + + +class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): + """ + Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 + (Redirecting to urls with special chars) + """ + def redirect_request(self, req, fp, code, msg, headers, new_url): + # + """ + Test if the new_url can be decoded to ascii + + :param req: + :param fp: + :param code: + :param msg: + :param headers: + :param new_url: + :return: + """ + try: + new_url.encode('latin1').decode('ascii') + fixed_url = new_url + except Exception: + # The url could not be decoded to ascii, so we do some url encoding + fixed_url = urllib.parse.quote(new_url.encode('latin1').decode('utf-8', 'replace'), safe='/:') + return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) + + +def get_user_agent(): + """ + Return a user agent customised for the platform the user is on. + """ + browser_list = USER_AGENTS.get(sys.platform, None) + if not browser_list: + browser_list = USER_AGENTS['default'] + random_index = randint(0, len(browser_list) - 1) + return browser_list[random_index] + + +def get_web_page(url, header=None, update_openlp=False): + """ + Attempts to download the webpage at url and returns that page or None. + + :param url: The URL to be downloaded. + :param header: An optional HTTP header to pass in the request to the web server. + :param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded. + Defaults to False. + """ + # TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a + # proxy_handler, build into an opener and install the opener into urllib2. + # http://docs.python.org/library/urllib2.html + if not url: + return None + # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 + opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) + urllib.request.install_opener(opener) + req = urllib.request.Request(url) + if not header or header[0].lower() != 'user-agent': + user_agent = get_user_agent() + req.add_header('User-Agent', user_agent) + if header: + req.add_header(header[0], header[1]) + log.debug('Downloading URL = %s' % url) + retries = 0 + while retries <= CONNECTION_RETRIES: + retries += 1 + time.sleep(0.1) + try: + page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT) + log.debug('Downloaded page {text}'.format(text=page.geturl())) + break + except urllib.error.URLError as err: + log.exception('URLError on {text}'.format(text=url)) + log.exception('URLError: {text}'.format(text=err.reason)) + page = None + if retries > CONNECTION_RETRIES: + raise + except socket.timeout: + log.exception('Socket timeout: {text}'.format(text=url)) + page = None + if retries > CONNECTION_RETRIES: + raise + except socket.gaierror: + log.exception('Socket gaierror: {text}'.format(text=url)) + page = None + if retries > CONNECTION_RETRIES: + raise + except ConnectionRefusedError: + log.exception('ConnectionRefused: {text}'.format(text=url)) + page = None + if retries > CONNECTION_RETRIES: + raise + break + except ConnectionError: + log.exception('Connection error: {text}'.format(text=url)) + page = None + if retries > CONNECTION_RETRIES: + raise + except HTTPException: + log.exception('HTTPException error: {text}'.format(text=url)) + page = None + if retries > CONNECTION_RETRIES: + raise + except: + # Don't know what's happening, so reraise the original + raise + if update_openlp: + Registry().get('application').process_events() + if not page: + log.exception('{text} could not be downloaded'.format(text=url)) + return None + log.debug(page) + return page + + +def get_url_file_size(url): + """ + Get the size of a file. + + :param url: The URL of the file we want to download. + """ + retries = 0 + while True: + try: + site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + meta = site.info() + return int(meta.get("Content-Length")) + except urllib.error.URLError: + if retries > CONNECTION_RETRIES: + raise + else: + retries += 1 + time.sleep(0.1) + continue + + +__all__ = ['get_web_page'] diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py new file mode 100644 index 000000000..7a8d478c5 --- /dev/null +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -0,0 +1,247 @@ +# -*- 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 openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size + +from tests.functional import MagicMock, patch + + +class TestHttpUtils(TestCase): + """ + A test suite to test out various methods around the AppLocation class. + """ + def test_get_user_agent_linux(self): + """ + Test that getting a user agent on Linux returns a user agent suitable for Linux + """ + with patch('openlp.core.common.httputils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'linux2' + + # WHEN: We call get_user_agent() + user_agent = get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + result = 'Linux' in user_agent or 'CrOS' in user_agent + self.assertTrue(result, 'The user agent should be a valid Linux user agent') + + def test_get_user_agent_windows(self): + """ + Test that getting a user agent on Windows returns a user agent suitable for Windows + """ + with patch('openlp.core.common.httputils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'win32' + + # WHEN: We call get_user_agent() + user_agent = get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent') + + def test_get_user_agent_macos(self): + """ + Test that getting a user agent on OS X returns a user agent suitable for OS X + """ + with patch('openlp.core.common.httputils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'darwin' + + # WHEN: We call get_user_agent() + user_agent = get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent') + + def test_get_user_agent_default(self): + """ + Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent + """ + with patch('openlp.core.common.httputils.sys') as mocked_sys: + + # GIVEN: The system is Linux + mocked_sys.platform = 'freebsd' + + # WHEN: We call get_user_agent() + user_agent = get_user_agent() + + # THEN: The user agent is a Linux (or ChromeOS) user agent + self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent') + + def test_get_web_page_no_url(self): + """ + Test that sending a URL of None to the get_web_page method returns None + """ + # GIVEN: A None url + test_url = None + + # WHEN: We try to get the test URL + result = get_web_page(test_url) + + # THEN: None should be returned + self.assertIsNone(result, 'The return value of get_web_page should be None') + + def test_get_web_page(self): + """ + Test that the get_web_page method works correctly + """ + with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.common.httputils.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() + MockRequest.return_value = mocked_request_object + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') + self.assertEqual(1, mocked_request_object.add_header.call_count, + 'There should only be 1 call to add_header') + mock_get_user_agent.assert_called_with() + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) + mocked_page_object.geturl.assert_called_with() + self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called') + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def test_get_web_page_with_header(self): + """ + Test that adding a header to the call to get_web_page() adds the header to the request + """ + with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.common.httputils.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 + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + fake_header = ('Fake-Header', 'fake value') + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, header=fake_header) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1]) + self.assertEqual(2, mocked_request_object.add_header.call_count, + 'There should only be 2 calls to add_header') + mock_get_user_agent.assert_called_with() + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) + mocked_page_object.geturl.assert_called_with() + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def test_get_web_page_with_user_agent_in_headers(self): + """ + 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.common.httputils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.common.httputils.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 + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + fake_url = 'this://is.a.fake/url' + user_agent_header = ('User-Agent', 'OpenLP/2.2.0') + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, header=user_agent_header) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1]) + self.assertEqual(1, mocked_request_object.add_header.call_count, + 'There should only be 1 call to add_header') + self.assertEqual(0, mock_get_user_agent.call_count, 'get_user_agent should not have been called') + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) + mocked_page_object.geturl.assert_called_with() + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def test_get_web_page_update_openlp(self): + """ + Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() + """ + with patch('openlp.core.common.httputils.urllib.request.Request') as MockRequest, \ + patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent, \ + patch('openlp.core.common.httputils.Registry') as MockRegistry: + # GIVEN: Mocked out objects, a fake URL + mocked_request_object = MagicMock() + MockRequest.return_value = mocked_request_object + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + mocked_registry_object = MagicMock() + mocked_application_object = MagicMock() + mocked_registry_object.get.return_value = mocked_application_object + MockRegistry.return_value = mocked_registry_object + fake_url = 'this://is.a.fake/url' + + # WHEN: The get_web_page() method is called + returned_page = get_web_page(fake_url, update_openlp=True) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + MockRequest.assert_called_with(fake_url) + mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent') + self.assertEqual(1, mocked_request_object.add_header.call_count, + 'There should only be 1 call to add_header') + mock_urlopen.assert_called_with(mocked_request_object, timeout=30) + mocked_page_object.geturl.assert_called_with() + mocked_registry_object.get.assert_called_with('application') + mocked_application_object.process_events.assert_called_with() + self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object') + + def test_get_url_file_size(self): + """ + Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events() + """ + with patch('openlp.core.common.httputils.urllib.request.urlopen') as mock_urlopen, \ + patch('openlp.core.common.httputils.get_user_agent') as mock_get_user_agent: + # GIVEN: Mocked out objects, a fake URL + mocked_page_object = MagicMock() + mock_urlopen.return_value = mocked_page_object + mock_get_user_agent.return_value = 'user_agent' + fake_url = 'this://is.a.fake/url' + + # WHEN: The get_url_file_size() method is called + size = get_url_file_size(fake_url) + + # THEN: The correct methods are called with the correct arguments and a web page is returned + mock_urlopen.assert_called_with(fake_url, timeout=30) From a368a1d69566ce94988333534ed27088120762b2 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 20 Dec 2016 21:59:40 +0000 Subject: [PATCH 34/40] Move urg_get_file --- openlp/core/common/httputils.py | 52 +++++++++++++++++++++++++++++- openlp/core/ui/firsttimeform.py | 57 +++------------------------------ 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index cab3ffb49..638e8a98a 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -22,7 +22,9 @@ """ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP. """ +import hashlib import logging +import os import socket import sys import time @@ -32,7 +34,7 @@ import urllib.request from http.client import HTTPException from random import randint -from openlp.core.common import Registry +from openlp.core.common import Registry, trace_error_handler log = logging.getLogger(__name__ + '.__init__') @@ -200,4 +202,52 @@ def get_url_file_size(url): continue +def url_get_file(callback, url, f_path, sha256=None): + """" + Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any + point. Returns False on download error. + + :param url: URL to download + :param f_path: Destination file + """ + block_count = 0 + block_size = 4096 + retries = 0 + while True: + try: + filename = open(f_path, "wb") + url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) + if sha256: + hasher = hashlib.sha256() + # Download until finished or canceled. + while not callback.was_cancelled: + data = url_file.read(block_size) + if not data: + break + filename.write(data) + if sha256: + hasher.update(data) + block_count += 1 + callback._download_progress(block_count, block_size) + filename.close() + if sha256 and hasher.hexdigest() != sha256: + log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) + os.remove(f_path) + return False + except (urllib.error.URLError, socket.timeout) as err: + trace_error_handler(log) + filename.close() + os.remove(f_path) + if retries > CONNECTION_RETRIES: + return False + else: + retries += 1 + time.sleep(0.1) + continue + break + # Delete file if cancelled, it may be a partial file. + if callback.was_cancelled: + os.remove(f_path) + return True + __all__ = ['get_web_page'] diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index b59f31211..d331bf843 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -22,7 +22,6 @@ """ This module contains the first time wizard. """ -import hashlib import logging import os import socket @@ -39,7 +38,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.common.httputils import get_web_page, get_url_file_size, CONNECTION_RETRIES, CONNECTION_TIMEOUT +from openlp.core.common.httputils import get_web_page, get_url_file_size, url_get_file, CONNECTION_TIMEOUT from .firsttimewizard import UiFirstTimeWizard, FirstTimePage log = logging.getLogger(__name__) @@ -395,54 +394,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.was_cancelled = True self.close() - def url_get_file(self, url, f_path, sha256=None): - """" - Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any - point. Returns False on download error. - - :param url: URL to download - :param f_path: Destination file - """ - block_count = 0 - block_size = 4096 - retries = 0 - while True: - try: - filename = open(f_path, "wb") - url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT) - if sha256: - hasher = hashlib.sha256() - # Download until finished or canceled. - while not self.was_cancelled: - data = url_file.read(block_size) - if not data: - break - filename.write(data) - if sha256: - hasher.update(data) - block_count += 1 - self._download_progress(block_count, block_size) - filename.close() - if sha256 and hasher.hexdigest() != sha256: - log.error('sha256 sums did not match for file: {file}'.format(file=f_path)) - os.remove(f_path) - return False - except (urllib.error.URLError, socket.timeout) as err: - trace_error_handler(log) - filename.close() - os.remove(f_path) - if retries > CONNECTION_RETRIES: - return False - else: - retries += 1 - time.sleep(0.1) - continue - break - # Delete file if cancelled, it may be a partial file. - if self.was_cancelled: - os.remove(f_path) - return True - def _build_theme_screenshots(self): """ This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``. @@ -616,7 +567,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=filename), 0) self.previous_size = 0 destination = os.path.join(songs_destination, str(filename)) - if not self.url_get_file('{path}{name}'.format(path=self.songs_url, name=filename), + if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), destination, sha256): missed_files.append('Song: {name}'.format(name=filename)) # Download Bibles @@ -628,7 +579,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): # TODO: Tested at home self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 - if not self.url_get_file('{path}{name}'.format(path=self.bibles_url, name=bible), + if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), os.path.join(bibles_destination, bible), sha256): missed_files.append('Bible: {name}'.format(name=bible)) @@ -643,7 +594,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.previous_size = 0 if not self.url_get_file('{path}{name}'.format(path=self.themes_url, name=theme), os.path.join(themes_destination, theme), - sha256): + sha256, self): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: file_list = '' From 8570fb5e8cf50aeb37a63870af7bdc6abed03246 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 09:41:57 +0000 Subject: [PATCH 35/40] Fix tests --- openlp/core/common/httputils.py | 2 ++ .../openlp_core_common/test_httputils.py | 19 ++++++++++++++++++- .../openlp_core_ui/test_firsttimeform.py | 18 ------------------ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 638e8a98a..b3cb50ef3 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -207,8 +207,10 @@ def url_get_file(callback, url, f_path, sha256=None): Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any point. Returns False on download error. + :param callback: the class which needs to be updated :param url: URL to download :param f_path: Destination file + :param sha256: The check sum value to be checked against the download value """ block_count = 0 block_size = 4096 diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 7a8d478c5..8709e9cfb 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -22,9 +22,11 @@ """ Functional tests to test the AppLocation class and related methods. """ +import socket +import os from unittest import TestCase -from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size +from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file from tests.functional import MagicMock, patch @@ -245,3 +247,18 @@ class TestHttpUtils(TestCase): # THEN: The correct methods are called with the correct arguments and a web page is returned mock_urlopen.assert_called_with(fake_url, timeout=30) + + @patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') + def test_socket_timeout(self, mocked_urlopen): + """ + Test socket timeout gets caught + """ + # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download + mocked_urlopen.side_effect = socket.timeout() + + # WHEN: Attempt to retrieve a file + url_get_file(url='http://localhost/test', f_path=self.tempfile) + + # THEN: socket.timeout should have been caught + # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files + self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') \ No newline at end of file diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 5dd1430cd..ec26f60fe 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -23,7 +23,6 @@ Package to test the openlp.core.ui.firsttimeform package. """ import os -import socket import tempfile import urllib from unittest import TestCase @@ -236,20 +235,3 @@ class TestFirstTimeForm(TestCase, TestMixin): # THEN: the critical_error_message_box should have been called self.assertEquals(mocked_message_box.mock_calls[1][1][0], 'Network Error 407', 'first_time_form should have caught Network Error') - - @patch('openlp.core.ui.firsttimeform.urllib.request.urlopen') - def test_socket_timeout(self, mocked_urlopen): - """ - Test socket timeout gets caught - """ - # GIVEN: Mocked urlopen to fake a network disconnect in the middle of a download - first_time_form = FirstTimeForm(None) - first_time_form.initialize(MagicMock()) - mocked_urlopen.side_effect = socket.timeout() - - # WHEN: Attempt to retrieve a file - first_time_form.url_get_file(url='http://localhost/test', f_path=self.tempfile) - - # THEN: socket.timeout should have been caught - # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files - self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') From a69df60978644d47365f7d98e0db059377c288ea Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 09:47:33 +0000 Subject: [PATCH 36/40] Fix tests again --- .../openlp_core_common/test_httputils.py | 18 ++++++++++++++---- .../openlp_core_ui/test_firsttimeform.py | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 8709e9cfb..6bf7aaa4c 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -22,19 +22,29 @@ """ Functional tests to test the AppLocation class and related methods. """ -import socket import os +import tempfile +import socket from unittest import TestCase from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file from tests.functional import MagicMock, patch +from tests.helpers.testmixin import TestMixin -class TestHttpUtils(TestCase): +class TestHttpUtils(TestCase, TestMixin): + """ - A test suite to test out various methods around the AppLocation class. + A test suite to test out various http helper functions. """ + def setUp(self): + self.tempfile = os.path.join(tempfile.gettempdir(), 'testfile') + + def tearDown(self): + if os.path.isfile(self.tempfile): + os.remove(self.tempfile) + def test_get_user_agent_linux(self): """ Test that getting a user agent on Linux returns a user agent suitable for Linux @@ -257,7 +267,7 @@ class TestHttpUtils(TestCase): mocked_urlopen.side_effect = socket.timeout() # WHEN: Attempt to retrieve a file - url_get_file(url='http://localhost/test', f_path=self.tempfile) + url_get_file(MagicMock(), url='http://localhost/test', f_path=self.tempfile) # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index ec26f60fe..ec77a3134 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -224,7 +224,8 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: Initial setup and mocks first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) - mocked_get_web_page.side_effect = urllib.error.HTTPError(url='http//localhost', + mocked_get_web_page.side_effect = urllib.error.HTTPError(MagicMock(), + url='http//localhost', code=407, msg='Network proxy error', hdrs=None, From 5896e053bc5c5705fff8b58e6058052f10f027fa Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 09:52:05 +0000 Subject: [PATCH 37/40] Fix tests again --- tests/functional/openlp_core_ui/test_firsttimeform.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index ec77a3134..ec26f60fe 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -224,8 +224,7 @@ class TestFirstTimeForm(TestCase, TestMixin): # GIVEN: Initial setup and mocks first_time_form = FirstTimeForm(None) first_time_form.initialize(MagicMock()) - mocked_get_web_page.side_effect = urllib.error.HTTPError(MagicMock(), - url='http//localhost', + mocked_get_web_page.side_effect = urllib.error.HTTPError(url='http//localhost', code=407, msg='Network proxy error', hdrs=None, From 7bd645bc7212fc83dfe9781eda289cd2126864c3 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 10:00:14 +0000 Subject: [PATCH 38/40] pep8 --- openlp/core/ui/firsttimeform.py | 6 +++--- tests/functional/openlp_core_common/test_httputils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index d331bf843..10456b979 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -568,7 +568,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.previous_size = 0 destination = os.path.join(songs_destination, str(filename)) if not url_get_file(self, '{path}{name}'.format(path=self.songs_url, name=filename), - destination, sha256): + destination, sha256): missed_files.append('Song: {name}'.format(name=filename)) # Download Bibles bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget) @@ -580,8 +580,8 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self._increment_progress_bar(self.downloading.format(name=bible), 0) self.previous_size = 0 if not url_get_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible), - os.path.join(bibles_destination, bible), - sha256): + os.path.join(bibles_destination, bible), + sha256): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 # Download themes diff --git a/tests/functional/openlp_core_common/test_httputils.py b/tests/functional/openlp_core_common/test_httputils.py index 6bf7aaa4c..98b24a994 100644 --- a/tests/functional/openlp_core_common/test_httputils.py +++ b/tests/functional/openlp_core_common/test_httputils.py @@ -271,4 +271,4 @@ class TestHttpUtils(TestCase, TestMixin): # THEN: socket.timeout should have been caught # NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files - self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') \ No newline at end of file + self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout') From 7323b3797d222391b0ac3b6b022f3ee82fc6f944 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 10:20:35 +0000 Subject: [PATCH 39/40] missed one --- openlp/core/ui/firsttimeform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 10456b979..3f56f4089 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -592,9 +592,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): # TODO: Tested at home self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 - if not self.url_get_file('{path}{name}'.format(path=self.themes_url, name=theme), + if not self.url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), os.path.join(themes_destination, theme), - sha256, self): + sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: file_list = '' From 0643ecdd103e3a13a7506399a663008e947eef0d Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 21 Dec 2016 12:46:35 +0000 Subject: [PATCH 40/40] Remove extra self --- openlp/core/ui/firsttimeform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 3f56f4089..8fd7a4f52 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -592,9 +592,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): # TODO: Tested at home self._increment_progress_bar(self.downloading.format(name=theme), 0) self.previous_size = 0 - if not self.url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), - os.path.join(themes_destination, theme), - sha256): + if not url_get_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), + os.path.join(themes_destination, theme), + sha256): missed_files.append('Theme: {name}'.format(name=theme)) if missed_files: file_list = ''