← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~phill-ridout/openlp/ftw-refactors into lp:openlp

 

Phill has proposed merging lp:~phill-ridout/openlp/ftw-refactors into lp:openlp.

Commit message:
Add proxy settings to ftw. Option to skip sample data

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/ftw-refactors/+merge/363747

Add option to skip downloading.
Add dialog for proxy settings to FTW
FTW refactors.
General fixes.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/ftw-refactors into lp:openlp.
=== modified file 'openlp/core/app.py'
--- openlp/core/app.py	2019-02-14 15:09:09 +0000
+++ openlp/core/app.py	2019-02-27 21:31:23 +0000
@@ -101,7 +101,7 @@
             ftw.initialize(screens)
             if ftw.exec() == QtWidgets.QDialog.Accepted:
                 Settings().setValue('core/has run wizard', True)
-            elif ftw.was_cancelled:
+            else:
                 QtCore.QCoreApplication.exit()
                 sys.exit()
         # Correct stylesheet bugs

=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py	2019-02-21 21:29:00 +0000
+++ openlp/core/common/httputils.py	2019-02-27 21:31:23 +0000
@@ -158,16 +158,20 @@
     return response.text
 
 
-def get_url_file_size(url):
+def get_url_file_size(url, proxy=None):
     """
     Get the size of a file.
 
     :param url: The URL of the file we want to download.
+    :param dict | ProxyMode | None proxy: ProxyMode enum or a dictionary containing the proxy servers, with their types
+        as the key e.g. {'http': 'http://proxyserver:port', 'https': 'https://proxyserver:port'}
     """
     retries = 0
+    if not isinstance(proxy, dict):
+        proxy = get_proxy_settings(mode=proxy)
     while True:
         try:
-            response = requests.head(url, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
+            response = requests.head(url, proxies=proxy, timeout=float(CONNECTION_TIMEOUT), allow_redirects=True)
             return int(response.headers['Content-Length'])
         except OSError:
             if retries > CONNECTION_RETRIES:
@@ -178,7 +182,7 @@
                 continue
 
 
-def download_file(update_object, url, file_path, sha256=None):
+def download_file(update_object, url, file_path, sha256=None, proxy=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.
@@ -187,15 +191,19 @@
     :param url: URL to download
     :param file_path: Destination file
     :param sha256: The check sum value to be checked against the download value
+    :param dict | ProxyMode | None proxy: ProxyMode enum or a dictionary containing the proxy servers, with their types
+        as the key e.g. {'http': 'http://proxyserver:port', 'https': 'https://proxyserver:port'}
     """
     block_count = 0
     block_size = 4096
     retries = 0
+    if not isinstance(proxy, dict):
+        proxy = get_proxy_settings(mode=proxy)
     log.debug('url_get_file: %s', url)
     while retries < CONNECTION_RETRIES:
         try:
             with file_path.open('wb') as saved_file:
-                response = requests.get(url, timeout=float(CONNECTION_TIMEOUT), stream=True)
+                response = requests.get(url,  proxies=proxy, timeout=float(CONNECTION_TIMEOUT), stream=True)
                 if sha256:
                     hasher = hashlib.sha256()
                 # Download until finished or canceled.
@@ -244,21 +252,21 @@
         """
         self._base_url = base_url
         self._file_name = file_name
-        self._download_cancelled = False
+        self.was_cancelled = False
         super().__init__()
 
     def start(self):
         """
         Download the url to the temporary directory
         """
-        if self._download_cancelled:
+        if self.was_cancelled:
             self.quit.emit()
             return
         try:
             dest_path = Path(gettempdir()) / 'openlp' / self._file_name
             url = '{url}{name}'.format(url=self._base_url, name=self._file_name)
             is_success = download_file(self, url, dest_path)
-            if is_success and not self._download_cancelled:
+            if is_success and not self.was_cancelled:
                 self.download_succeeded.emit(dest_path)
             else:
                 self.download_failed.emit()
@@ -273,4 +281,4 @@
         """
         A slot to allow the download to be cancelled from outside of the thread
         """
-        self._download_cancelled = True
+        self.was_cancelled = True

=== modified file 'openlp/core/threading.py'
--- openlp/core/threading.py	2019-02-14 15:09:09 +0000
+++ openlp/core/threading.py	2019-02-27 21:31:23 +0000
@@ -76,11 +76,11 @@
     Get the worker by the thread name
 
     :param str thread_name: The name of the thread
-    :returns: The worker for this thread name
+    :returns: The worker for this thread name, or None
     """
     thread_info = Registry().get('application').worker_threads.get(thread_name)
     if not thread_info:
-        raise KeyError('No thread named "{}" exists'.format(thread_name))
+        return
     return thread_info.get('worker')
 
 

=== modified file 'openlp/core/ui/firsttimeform.py'
--- openlp/core/ui/firsttimeform.py	2019-02-21 21:29:00 +0000
+++ openlp/core/ui/firsttimeform.py	2019-02-27 21:31:23 +0000
@@ -46,6 +46,7 @@
 from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread
 from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard
 from openlp.core.ui.icons import UiIcons
+from openlp.core.widgets.widgets import ProxyDialog
 
 
 log = logging.getLogger(__name__)
@@ -91,7 +92,7 @@
 
 class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
     """
-    This is the Theme Import Wizard, which allows easy creation and editing of OpenLP themes.
+    This is the FirstTimeWizard, designed to help new users to get up and running quickly.
     """
     log.info('ThemeWizardForm loaded')
 
@@ -103,6 +104,7 @@
         self.web_access = True
         self.web = ''
         self.setup_ui(self)
+        self.customButtonClicked.connect(self._on_custom_button_clicked)
         self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed)
         self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection)
         self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll)
@@ -111,13 +113,13 @@
         """
         Returns the id of the next FirstTimePage to go to based on enabled plugins
         """
-        if FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
+        if FirstTimePage.Download < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
             # If the songs plugin is enabled then go to the songs page
             return FirstTimePage.Songs
-        elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
+        elif FirstTimePage.Download < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
             # Otherwise, if the Bibles plugin is enabled then go to the Bibles page
             return FirstTimePage.Bibles
-        elif FirstTimePage.ScreenConfig < self.currentId() < FirstTimePage.Themes:
+        elif FirstTimePage.Download < self.currentId() < FirstTimePage.Themes:
             # Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes
             return FirstTimePage.Themes
         else:
@@ -133,9 +135,7 @@
             if not self.web_access:
                 return FirstTimePage.NoInternet
             else:
-                return FirstTimePage.Plugins
-        elif self.currentId() == FirstTimePage.Plugins:
-            return self.get_next_page_id()
+                return FirstTimePage.Songs
         elif self.currentId() == FirstTimePage.Progress:
             return -1
         elif self.currentId() == FirstTimePage.NoInternet:
@@ -147,7 +147,7 @@
         Run the wizard.
         """
         self.set_defaults()
-        return QtWidgets.QWizard.exec(self)
+        return super().exec()
 
     def initialize(self, screens):
         """
@@ -227,17 +227,13 @@
         """
         self.restart()
         self.web = 'https://get.openlp.org/ftw/'
-        self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
-        self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked)
-        self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked)
         self.currentIdChanged.connect(self.on_current_id_changed)
         Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
-        self.no_internet_finish_button.setVisible(False)
-        self.no_internet_cancel_button.setVisible(False)
         # Check if this is a re-run of the wizard.
         self.has_run_wizard = Settings().value('core/has run wizard')
         create_paths(Path(gettempdir(), 'openlp'))
         self.theme_combo_box.clear()
+        self.button(QtWidgets.QWizard.CustomButton1).setVisible(False)
         if self.has_run_wizard:
             self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
             self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
@@ -260,57 +256,85 @@
         """
         Detects Page changes and updates as appropriate.
         """
-        # Keep track of the page we are at.  Triggering "Cancel" causes page_id to be a -1.
+        back_button = self.button(QtWidgets.QWizard.BackButton)
+        cancel_button = self.button(QtWidgets.QWizard.CancelButton)
+        internet_settings_button = self.button(QtWidgets.QWizard.CustomButton1)
+        next_button = self.button(QtWidgets.QWizard.NextButton)
+        back_button.setVisible(True)
+        next_button.setVisible(True)
+        internet_settings_button.setVisible(False)
         self.application.process_events()
-        if page_id != -1:
-            self.last_id = page_id
-        if page_id == FirstTimePage.Download:
-            self.back_button.setVisible(False)
-            self.next_button.setVisible(False)
-            # Set the no internet page text.
-            if self.has_run_wizard:
-                self.no_internet_label.setText(self.no_internet_text)
-            else:
-                self.no_internet_label.setText(self.no_internet_text + self.cancel_wizard_text)
+        if page_id == FirstTimePage.SampleOption:
+            internet_settings_button.setVisible(True)
+        elif page_id == FirstTimePage.Download:
+            back_button.setVisible(False)
+            next_button.setVisible(False)
             self.application.set_busy_cursor()
             self._download_index()
             self.application.set_normal_cursor()
-            self.back_button.setVisible(False)
-            self.next_button.setVisible(True)
             self.next()
         elif page_id == FirstTimePage.NoInternet:
-            self.back_button.setVisible(False)
-            self.next_button.setVisible(False)
-            self.cancel_button.setVisible(False)
-            self.no_internet_finish_button.setVisible(True)
-            if self.has_run_wizard:
-                self.no_internet_cancel_button.setVisible(False)
-            else:
-                self.no_internet_cancel_button.setVisible(True)
-        elif page_id == FirstTimePage.Plugins:
-            self.back_button.setVisible(False)
+            next_button.setVisible(False)
+            cancel_button.setVisible(False)
+            internet_settings_button.setVisible(True)
         elif page_id == FirstTimePage.Progress:
+            back_button.setVisible(False)
+            next_button.setVisible(False)
             self.application.set_busy_cursor()
             self._pre_wizard()
             self._perform_wizard()
             self._post_wizard()
             self.application.set_normal_cursor()
 
-    def on_cancel_button_clicked(self):
-        """
-        Process the triggering of the cancel button.
+    def accept(self):
+        """
+        Called when the user clicks 'Finish'. Reimplement it to to save the plugin status
+
+        :rtype: None
+        """
+        self._set_plugin_status(self.songs_check_box, 'songs/status')
+        self._set_plugin_status(self.bible_check_box, 'bibles/status')
+        self._set_plugin_status(self.presentation_check_box, 'presentations/status')
+        self._set_plugin_status(self.image_check_box, 'images/status')
+        self._set_plugin_status(self.media_check_box, 'media/status')
+        self._set_plugin_status(self.custom_check_box, 'custom/status')
+        self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
+        self._set_plugin_status(self.alert_check_box, 'alerts/status')
+        self.screen_selection_widget.save()
+        if self.theme_combo_box.currentIndex() != -1:
+            Settings().setValue('themes/global theme', self.theme_combo_box.currentText())
+        super().accept()
+
+    def reject(self):
+        """
+        Called when the user clicks the cancel button. Reimplement it to clean up the threads.
+
+        :rtype: None
         """
         self.was_cancelled = True
-        if self.thumbnail_download_threads:  # TODO: Use main thread list
-            for thread_name in self.thumbnail_download_threads:
-                worker = get_thread_worker(thread_name)
-                if worker:
-                    worker.cancel_download()
+        for thread_name in self.thumbnail_download_threads:
+            worker = get_thread_worker(thread_name)
+            if worker:
+                worker.cancel_download()
         # Was the thread created.
         if self.thumbnail_download_threads:
             while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]):
                 time.sleep(0.1)
         self.application.set_normal_cursor()
+        super().reject()
+
+    def _on_custom_button_clicked(self, which):
+        """
+        Slot to handle the a click on one of the wizards custom buttons.
+
+        :param int QtWidgets.QWizard which: The button pressed
+        :rtype: None
+        """
+        # Internet settings button
+        if which == QtWidgets.QWizard.CustomButton1:
+            proxy_dialog = ProxyDialog(self)
+            proxy_dialog.retranslate_ui()
+            proxy_dialog.exec()
 
     def on_themes_list_widget_selection_changed(self):
         """
@@ -330,23 +354,6 @@
                 elif not item.isSelected() and cbox_index != -1:
                     self.theme_combo_box.removeItem(cbox_index)
 
-    def on_no_internet_finish_button_clicked(self):
-        """
-        Process the triggering of the "Finish" button on the No Internet page.
-        """
-        self.application.set_busy_cursor()
-        self._perform_wizard()
-        self.application.set_normal_cursor()
-        Settings().setValue('core/has run wizard', True)
-        self.close()
-
-    def on_no_internet_cancel_button_clicked(self):
-        """
-        Process the triggering of the "Cancel" button on the No Internet page.
-        """
-        self.was_cancelled = True
-        self.close()
-
     def update_progress(self, count, block_size):
         """
         Calculate and display the download progress. This method is called by download_file().
@@ -373,7 +380,7 @@
         Prepare the UI for the process.
         """
         self.max_progress = 0
-        self.finish_button.setVisible(False)
+        self.button(QtWidgets.QWizard.FinishButton).setEnabled(False)
         self.application.process_events()
         try:
             # Loop through the songs list and increase for each selected item
@@ -428,58 +435,36 @@
         """
         Clean up the UI after the process has finished.
         """
+        complete_str = ''
         if self.max_progress:
             self.progress_bar.setValue(self.progress_bar.maximum())
             if self.has_run_wizard:
                 text = translate('OpenLP.FirstTimeWizard',
-                                 'Download complete. Click the {button} button to return to OpenLP.'
-                                 ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton)))
-                self.progress_label.setText(text)
+                                 'Download complete. Click the \'{finish_button}\' button to return to OpenLP.')
             else:
                 text = translate('OpenLP.FirstTimeWizard',
-                                 'Download complete. Click the {button} button to start OpenLP.'
-                                 ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton)))
-                self.progress_label.setText(text)
+                                 'Download complete. Click the \'{finish_button}\' button to start OpenLP.')
         else:
             if self.has_run_wizard:
-                text = translate('OpenLP.FirstTimeWizard',
-                                 'Click the {button} button to return to OpenLP.'
-                                 ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton)))
-                self.progress_label.setText(text)
+                text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to return to OpenLP.')
             else:
-                text = translate('OpenLP.FirstTimeWizard',
-                                 'Click the {button} button to start OpenLP.'
-                                 ).format(button=clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton)))
-                self.progress_label.setText(text)
-        self.finish_button.setVisible(True)
-        self.finish_button.setEnabled(True)
-        self.cancel_button.setVisible(False)
-        self.next_button.setVisible(False)
+                text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to start OpenLP.')
+        self.progress_label.setText(text.format(finish_button=self.finish_button_text))
+        self.button(QtWidgets.QWizard.FinishButton).setEnabled(True)
+        self.button(QtWidgets.QWizard.CancelButton).setVisible(False)
         self.application.process_events()
 
     def _perform_wizard(self):
         """
         Run the tasks in the wizard.
         """
-        # Set plugin states
-        self._increment_progress_bar(translate('OpenLP.FirstTimeWizard', 'Enabling selected plugins...'))
-        self._set_plugin_status(self.songs_check_box, 'songs/status')
-        self._set_plugin_status(self.bible_check_box, 'bibles/status')
-        self._set_plugin_status(self.presentation_check_box, 'presentations/status')
-        self._set_plugin_status(self.image_check_box, 'images/status')
-        self._set_plugin_status(self.media_check_box, 'media/status')
-        self._set_plugin_status(self.custom_check_box, 'custom/status')
-        self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
-        self._set_plugin_status(self.alert_check_box, 'alerts/status')
+
         if self.web_access:
             if not self._download_selected():
                 critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
                                            translate('OpenLP.FirstTimeWizard', 'There was a connection problem while '
                                                      'downloading, so further downloads will be skipped. Try to re-run '
                                                      'the First Time Wizard later.'))
-        self.screen_selection_widget.save()
-        if self.theme_combo_box.currentIndex() != -1:
-            Settings().setValue('themes/global theme', self.theme_combo_box.currentText())
 
     def _download_selected(self):
         """

=== modified file 'openlp/core/ui/firsttimewizard.py'
--- openlp/core/ui/firsttimewizard.py	2019-02-15 22:34:53 +0000
+++ openlp/core/ui/firsttimewizard.py	2019-02-27 21:31:23 +0000
@@ -39,14 +39,15 @@
     An enumeration class with each of the pages of the wizard.
     """
     Welcome = 0
-    ScreenConfig = 1
-    Download = 2
-    NoInternet = 3
-    Plugins = 4
-    Songs = 5
-    Bibles = 6
-    Themes = 7
-    Progress = 8
+    Plugins = 1
+    ScreenConfig = 2
+    SampleOption = 3
+    Download = 4
+    NoInternet = 5
+    Songs = 6
+    Bibles = 7
+    Themes = 8
+    Progress = 9
 
 
 class ThemeListWidget(QtWidgets.QListWidget):
@@ -97,20 +98,13 @@
         first_time_wizard.resize(550, 386)
         first_time_wizard.setModal(True)
         first_time_wizard.setOptions(QtWidgets.QWizard.IndependentPages | QtWidgets.QWizard.NoBackButtonOnStartPage |
-                                     QtWidgets.QWizard.NoBackButtonOnLastPage | QtWidgets.QWizard.HaveCustomButton1 |
-                                     QtWidgets.QWizard.HaveCustomButton2)
+                                     QtWidgets.QWizard.NoBackButtonOnLastPage | QtWidgets.QWizard.HaveCustomButton1)
         if is_macosx():                                                                             # pragma: nocover
             first_time_wizard.setPixmap(QtWidgets.QWizard.BackgroundPixmap,
                                         QtGui.QPixmap(':/wizards/openlp-osx-wizard.png'))
             first_time_wizard.resize(634, 386)
         else:
             first_time_wizard.setWizardStyle(QtWidgets.QWizard.ModernStyle)
-        self.finish_button = self.button(QtWidgets.QWizard.FinishButton)
-        self.no_internet_finish_button = self.button(QtWidgets.QWizard.CustomButton1)
-        self.cancel_button = self.button(QtWidgets.QWizard.CancelButton)
-        self.no_internet_cancel_button = self.button(QtWidgets.QWizard.CustomButton2)
-        self.next_button = self.button(QtWidgets.QWizard.NextButton)
-        self.back_button = self.button(QtWidgets.QWizard.BackButton)
         add_welcome_page(first_time_wizard, ':/wizards/wizard_firsttime.bmp')
         # The screen config page
         self.screen_page = QtWidgets.QWizardPage()
@@ -121,6 +115,18 @@
         self.screen_selection_widget.load()
         self.screen_page_layout.addRow(self.screen_selection_widget)
         first_time_wizard.setPage(FirstTimePage.ScreenConfig, self.screen_page)
+        # Download Samples page
+        self.resource_page = QtWidgets.QWizardPage()
+        self.resource_page.setObjectName('resource_page')
+        self.resource_page.setFinalPage(True)
+        self.resource_layout = QtWidgets.QVBoxLayout(self.resource_page)
+        self.resource_layout.setContentsMargins(50, 20, 50, 20)
+        self.resource_layout.setObjectName('resource_layout')
+        self.resource_label = QtWidgets.QLabel(self.resource_page)
+        self.resource_label.setObjectName('resource_label')
+        self.resource_label.setWordWrap(True)
+        self.resource_layout.addWidget(self.resource_label)
+        first_time_wizard.setPage(FirstTimePage.SampleOption, self.resource_page)
         # The download page
         self.download_page = QtWidgets.QWizardPage()
         self.download_page.setObjectName('download_page')
@@ -134,6 +140,7 @@
         # The "you don't have an internet connection" page.
         self.no_internet_page = QtWidgets.QWizardPage()
         self.no_internet_page.setObjectName('no_internet_page')
+        self.no_internet_page.setFinalPage(True)
         self.no_internet_layout = QtWidgets.QVBoxLayout(self.no_internet_page)
         self.no_internet_layout.setContentsMargins(50, 30, 50, 40)
         self.no_internet_layout.setObjectName('no_internet_layout')
@@ -242,27 +249,32 @@
         self.progress_bar.setObjectName('progress_bar')
         self.progress_layout.addWidget(self.progress_bar)
         first_time_wizard.setPage(FirstTimePage.Progress, self.progress_page)
-        self.retranslate_ui(first_time_wizard)
+        self.retranslate_ui()
 
-    def retranslate_ui(self, first_time_wizard):
+    def retranslate_ui(self):
         """
         Translate the UI on the fly
 
         :param first_time_wizard: The wizard form
         """
-        first_time_wizard.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard'))
+        self.finish_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.FinishButton))
+        back_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.BackButton))
+        next_button_text = clean_button_text(self.buttonText(QtWidgets.QWizard.NextButton))
+
+        self.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'First Time Wizard'))
         text = translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')
-        first_time_wizard.title_label.setText('<span style="font-size:14pt; font-weight:600;">{text}'
-                                              '</span>'.format(text=text))
-        button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.NextButton))
-        first_time_wizard.information_label.setText(
+        self.title_label.setText('<span style="font-size:14pt; font-weight:600;">{text}</span>'.format(text=text))
+        self.information_label.setText(
             translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. '
-                                                'Click the {button} button below to start.').format(button=button))
+                                                'Click the \'{next_button}\' button below to start.'
+                      ).format(next_button=next_button_text))
+        self.setButtonText(
+            QtWidgets.QWizard.CustomButton1, translate('OpenLP.FirstTimeWizard', 'Internet Settings'))
         self.download_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading Resource Index'))
-        self.download_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while the resource index is '
-                                                                           'downloaded.'))
-        self.download_label.setText(translate('OpenLP.FirstTimeWizard', 'Please wait while OpenLP downloads the '
-                                                                        'resource index file...'))
+        self.download_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
+                                                 'Please wait while the resource index is downloaded.'))
+        self.download_label.setText(translate('OpenLP.FirstTimeWizard',
+                                              'Please wait while OpenLP downloads the resource index file...'))
         self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Select parts of the program you wish to use'))
         self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
                                                'You can also change these settings after the Wizard.'))
@@ -270,11 +282,10 @@
         self.screen_page.setSubTitle(translate('OpenLP.FirstTimeWizard',
                                                'Choose the main display screen for OpenLP.'))
         self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs'))
-        self.custom_check_box.setText(translate('OpenLP.FirstTimeWizard',
-                                                'Custom Slides – Easier to manage than songs and they have their own'
-                                                ' list of slides'))
-        self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard',
-                                               'Bibles – Import and show Bibles'))
+        self.custom_check_box.setText(
+            translate('OpenLP.FirstTimeWizard',
+                      'Custom Slides – Easier to manage than songs and they have their own list of slides'))
+        self.bible_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Bibles – Import and show Bibles'))
         self.image_check_box.setText(translate('OpenLP.FirstTimeWizard',
                                                'Images – Show images or replace background with them'))
         self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
@@ -283,22 +294,25 @@
         self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
         self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
                                                'Alerts – Display informative messages while showing other slides'))
+        self.resource_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Resource Data'))
+        self.resource_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Can OpenLP download some resource data?'))
+        self.resource_label.setText(
+            translate('OpenLP.FirstTimeWizard',
+                      'OpenLP has collected some resources that we have permission to distribute.\n\n'
+                      'If you would like to download some of these resources click the \'{next_button}\' button, '
+                      'otherwise click the \'{finish_button}\' button.'
+                      ).format(next_button=next_button_text, finish_button=self.finish_button_text))
         self.no_internet_page.setTitle(translate('OpenLP.FirstTimeWizard', 'No Internet Connection'))
-        self.no_internet_page.setSubTitle(
-            translate('OpenLP.FirstTimeWizard', 'Unable to detect an Internet connection.'))
-        button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton))
-        self.no_internet_text = translate('OpenLP.FirstTimeWizard',
-                                          'No Internet connection was found. The First Time Wizard needs an Internet '
-                                          'connection in order to be able to download sample songs, Bibles and themes.'
-                                          '  Click the {button} button now to start OpenLP with initial settings and '
-                                          'no sample data.\n\nTo re-run the First Time Wizard and import this sample '
-                                          'data at a later time, check your Internet connection and re-run this '
-                                          'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.'
-                                          ).format(button=button)
-        button = clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.CancelButton))
-        self.cancel_wizard_text = translate('OpenLP.FirstTimeWizard',
-                                            '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), '
-                                            'click the {button} button now.').format(button=button)
+        self.no_internet_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Cannot connect to the internet.'))
+        self.no_internet_label.setText(
+            translate('OpenLP.FirstTimeWizard',
+                      'OpenLP could not connect to the internet to get information about the sample data available.\n\n'
+                      'Please check your internet connection. If your church uses a proxy server click the '
+                      '\'Internet Settings\' button below and enter the server details there.\n\nClick the '
+                      '\'{back_button}\' button to try again.\n\nIf you click the \'{finish_button}\' '
+                      'button you can download the data at a later time by selecting \'Re-run First Time Wizard\' '
+                      'from the \'Tools\' menu in OpenLP.'
+                      ).format(back_button=back_button_text, finish_button=self.finish_button_text))
         self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs'))
         self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.'))
         self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles'))
@@ -310,13 +324,8 @@
         self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all'))
         self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all'))
         self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring'))
-        self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded '
-                                                                           'and OpenLP is configured.'))
-        self.progress_label.setText(translate('OpenLP.FirstTimeWizard', 'Starting configuration process...'))
-        first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton1,
-                                        clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.FinishButton)))
-        first_time_wizard.setButtonText(QtWidgets.QWizard.CustomButton2,
-                                        clean_button_text(first_time_wizard.buttonText(QtWidgets.QWizard.CancelButton)))
+        self.progress_page.setSubTitle(
+            translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded and OpenLP is configured.'))
 
     def on_projectors_check_box_clicked(self):
         # When clicking projectors_check box, change the visibility setting for Projectors panel.

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2019-02-14 15:09:09 +0000
+++ openlp/core/ui/mainwindow.py	2019-02-27 21:31:23 +0000
@@ -681,8 +681,7 @@
             return
         first_run_wizard = FirstTimeForm(self)
         first_run_wizard.initialize(ScreenList())
-        first_run_wizard.exec()
-        if first_run_wizard.was_cancelled:
+        if first_run_wizard.exec() == QtWidgets.QDialog.Rejected:
             return
         self.application.set_busy_cursor()
         self.first_time()

=== modified file 'openlp/core/widgets/widgets.py'
--- openlp/core/widgets/widgets.py	2019-02-14 15:09:09 +0000
+++ openlp/core/widgets/widgets.py	2019-02-27 21:31:23 +0000
@@ -150,6 +150,34 @@
         settings.setValue('advanced/proxy password', self.password_edit.text())
 
 
+class ProxyDialog(QtWidgets.QDialog):
+    """
+    A basic dialog to show proxy settingd
+    """
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.layout = QtWidgets.QVBoxLayout(self)
+        self.proxy_widget = ProxyWidget(self)
+        self.layout.addWidget(self.proxy_widget)
+        self.button_box = \
+            QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, self)
+        self.layout.addWidget(self.button_box)
+        self.button_box.accepted.connect(self.accept)
+        self.button_box.rejected.connect(self.reject)
+
+    def accept(self):
+        """
+        Reimplement the the accept slot so that the ProxyWidget settings can be saved.
+        :rtype: None
+        """
+        self.proxy_widget.save()
+        super().accept()
+
+    def retranslate_ui(self):
+        self.proxy_widget.retranslate_ui()
+        self.setWindowTitle(translate('OpenLP.ProxyDialog', 'Proxy Server Settings'))
+
+
 class ScreenButton(QtWidgets.QPushButton):
     """
     A special button class that holds the screen information about it

=== modified file 'openlp/plugins/songs/lib/importers/openlp.py'
--- openlp/plugins/songs/lib/importers/openlp.py	2019-02-14 15:09:09 +0000
+++ openlp/plugins/songs/lib/importers/openlp.py	2019-02-27 21:31:23 +0000
@@ -106,7 +106,7 @@
             pass
 
         # Check the file type
-        if not isinstance(self.import_source, str) or not self.import_source.endswith('.sqlite'):
+        if self.import_source.suffix != '.sqlite':
             self.log_error(self.import_source, translate('SongsPlugin.OpenLPSongImport',
                                                          'Not a valid OpenLP 2 song database.'))
             return

=== modified file 'openlp/plugins/songs/reporting.py'
--- openlp/plugins/songs/reporting.py	2019-02-14 15:09:09 +0000
+++ openlp/plugins/songs/reporting.py	2019-02-27 21:31:23 +0000
@@ -49,13 +49,6 @@
         Path(translate('SongPlugin.ReportSongList', 'song_extract.csv')),
         translate('SongPlugin.ReportSongList', 'CSV format (*.csv)'))
 
-    if report_file_path is None:
-        main_window.error_message(
-            translate('SongPlugin.ReportSongList', 'Output Path Not Selected'),
-            translate('SongPlugin.ReportSongList', 'You have not set a valid output location for your report. \n'
-                                                   'Please select an existing path on your computer.')
-        )
-        return
     report_file_path.with_suffix('.csv')
     Registry().get('application').set_busy_cursor()
     try:

=== modified file 'run_openlp.py' (properties changed: +x to -x)
=== modified file 'setup.py' (properties changed: +x to -x)
=== modified file 'tests/functional/openlp_core/common/test_httputils.py'
--- tests/functional/openlp_core/common/test_httputils.py	2019-02-14 15:09:09 +0000
+++ tests/functional/openlp_core/common/test_httputils.py	2019-02-27 21:31:23 +0000
@@ -224,7 +224,7 @@
         file_size = get_url_file_size(fake_url)
 
         # THEN: The correct methods are called with the correct arguments and a web page is returned
-        mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, timeout=30.0)
+        mocked_requests.head.assert_called_once_with(fake_url, allow_redirects=True, proxies=None, timeout=30.0)
         assert file_size == 100
 
     @patch('openlp.core.common.httputils.requests')

=== modified file 'tests/functional/openlp_core/ui/test_firsttimeform.py'
--- tests/functional/openlp_core/ui/test_firsttimeform.py	2019-02-16 08:57:11 +0000
+++ tests/functional/openlp_core/ui/test_firsttimeform.py	2019-02-27 21:31:23 +0000
@@ -27,6 +27,8 @@
 from unittest import TestCase
 from unittest.mock import MagicMock, call, patch, DEFAULT
 
+from PyQt5 import QtWidgets
+
 from openlp.core.common.path import Path
 from openlp.core.common.registry import Registry
 from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem
@@ -120,10 +122,23 @@
         # THEN: The screens should be set up, and the default values initialised
         assert expected_screens == frw.screens, 'The screens should be correct'
         assert frw.web_access is True, 'The default value of self.web_access should be True'
-        assert frw.was_cancelled is False, 'The default value of self.was_cancelled should be False'
         assert [] == frw.thumbnail_download_threads, 'The list of threads should be empty'
         assert frw.has_run_wizard is False, 'has_run_wizard should be False'
 
+    @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.exec')
+    def test_exec(self, mocked_qwizard_exec):
+
+        # GIVEN: An instance of FirstTimeForm
+        frw = FirstTimeForm(None)
+        with patch.object(frw, 'set_defaults') as mocked_set_defaults:
+
+            # WHEN: exec is called
+            frw.exec()
+
+            # THEN: The wizard should be reset and the exec methon on the super class should have been called
+            mocked_set_defaults.assert_called_once()
+            mocked_qwizard_exec.assert_called_once()
+
     def test_set_defaults(self):
         """
         Test that the default values are set when set_defaults() is run
@@ -134,8 +149,6 @@
         mocked_settings = MagicMock()
         mocked_settings.value.side_effect = lambda key: {'core/has run wizard': False}[key]
         with patch.object(frw, 'restart') as mocked_restart, \
-                patch.object(frw, 'cancel_button') as mocked_cancel_button, \
-                patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
                 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
                 patch.object(frw, 'theme_combo_box') as mocked_theme_combo_box, \
                 patch.object(frw, 'songs_check_box') as mocked_songs_check_box, \
@@ -153,12 +166,8 @@
             # THEN: The default values should have been set
             mocked_restart.assert_called_once()
             assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
-            mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
-            mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
-                frw.on_no_internet_finish_button_clicked)
             mocked_currentIdChanged.connect.assert_called_once_with(frw.on_current_id_changed)
             mocked_register_function.assert_called_once_with('config_screen_changed', frw.screen_selection_widget.load)
-            mocked_no_internet_finish_btn.setVisible.assert_called_once_with(False)
             mocked_settings.value.assert_has_calls([call('core/has run wizard')])
             mocked_gettempdir.assert_called_once()
             mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
@@ -177,8 +186,6 @@
         mocked_settings.value.side_effect = \
             lambda key: {'core/has run wizard': True, 'themes/global theme': 'Default Theme'}[key]
         with patch.object(frw, 'restart') as mocked_restart, \
-                patch.object(frw, 'cancel_button') as mocked_cancel_button, \
-                patch.object(frw, 'no_internet_finish_button') as mocked_no_internet_finish_btn, \
                 patch.object(frw, 'currentIdChanged') as mocked_currentIdChanged, \
                 patch.object(frw, 'theme_combo_box', **{'findText.return_value': 3}) as mocked_theme_combo_box, \
                 patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT,
@@ -200,12 +207,8 @@
             # THEN: The default values should have been set
             mocked_restart.assert_called_once()
             assert 'https://get.openlp.org/ftw/' == frw.web, 'The default URL should be set'
-            mocked_cancel_button.clicked.connect.assert_called_once_with(frw.on_cancel_button_clicked)
-            mocked_no_internet_finish_btn.clicked.connect.assert_called_once_with(
-                frw.on_no_internet_finish_button_clicked)
             mocked_currentIdChanged.connect.assert_called_once_with(frw.on_current_id_changed)
             mocked_register_function.assert_called_once_with('config_screen_changed', frw.screen_selection_widget.load)
-            mocked_no_internet_finish_btn.setVisible.assert_called_once_with(False)
             mocked_settings.value.assert_has_calls([call('core/has run wizard'), call('themes/global theme')])
             mocked_gettempdir.assert_called_once()
             mocked_create_paths.assert_called_once_with(Path('temp', 'openlp'))
@@ -219,12 +222,79 @@
             mocked_theme_combo_box.findText.assert_called_once_with('Default Theme')
             mocked_theme_combo_box.setCurrentIndex(3)
 
+    @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.accept')
+    @patch('openlp.core.ui.firsttimewizard.Settings')
+    def test_accept_method(self, mocked_settings, mocked_qwizard_accept):
+        """
+        Test the FirstTimeForm.accept method
+        """
+        # GIVEN: An instance of FirstTimeForm
+        frw = FirstTimeForm(None)
+        with patch.object(frw, '_set_plugin_status') as mocked_set_plugin_status, \
+                patch.multiple(frw, songs_check_box=DEFAULT, bible_check_box=DEFAULT, presentation_check_box=DEFAULT,
+                               image_check_box=DEFAULT, media_check_box=DEFAULT, custom_check_box=DEFAULT,
+                               song_usage_check_box=DEFAULT, alert_check_box=DEFAULT) as mocked_check_boxes, \
+                patch.object(frw, 'screen_selection_widget') as mocked_screen_selection_widget:
+
+            # WHEN: Calling accept
+            frw.accept()
+
+            # THEN: The selected plugins should be enabled, the screen selection saved and the super method called
+            mocked_set_plugin_status.assert_has_calls([
+                call(mocked_check_boxes['songs_check_box'], 'songs/status'),
+                call(mocked_check_boxes['bible_check_box'], 'bibles/status'),
+                call(mocked_check_boxes['presentation_check_box'], 'presentations/status'),
+                call(mocked_check_boxes['image_check_box'], 'images/status'),
+                call(mocked_check_boxes['media_check_box'], 'media/status'),
+                call(mocked_check_boxes['custom_check_box'], 'custom/status'),
+                call(mocked_check_boxes['song_usage_check_box'], 'songusage/status'),
+                call(mocked_check_boxes['alert_check_box'], 'alerts/status')])
+            mocked_screen_selection_widget.save.assert_called_once()
+            mocked_qwizard_accept.assert_called_once()
+
+    @patch('openlp.core.ui.firsttimewizard.Settings')
+    def test_accept_method_theme_not_selected(self, mocked_settings):
+        """
+        Test the FirstTimeForm.accept method when there is no default theme selected
+        """
+        # GIVEN: An instance of FirstTimeForm
+        frw = FirstTimeForm(None)
+        with patch.object(frw, '_set_plugin_status'), \
+                patch.object(frw, 'screen_selection_widget'), \
+                patch.object(frw, 'theme_combo_box', **{'currentIndex.return_value': '-1'}):
+
+            # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns -1
+            frw.accept()
+
+            # THEN: OpenLP should not try to save a theme name
+            mocked_settings.setValue.assert_not_called()
+
+    @patch('openlp.core.ui.firsttimeform.Settings')
+    def test_accept_method_theme_selected(self, mocked_settings):
+        """
+        Test the FirstTimeForm.accept method when a default theme is selected
+        """
+        # GIVEN: An instance of FirstTimeForm
+        frw = FirstTimeForm(None)
+        with patch.object(frw, '_set_plugin_status'), \
+             patch.object(frw, 'screen_selection_widget'), \
+             patch.object(
+                 frw, 'theme_combo_box', **{'currentIndex.return_value': 0, 'currentText.return_value': 'Test Item'}):
+
+            # WHEN: Calling accept and the currentIndex method of the theme_combo_box returns 0
+            frw.accept()
+
+            # THEN: The 'currentItem' in the combobox should have been set as the default theme.
+            mocked_settings().setValue.assert_called_once_with('themes/global theme', 'Test Item')
+
+    @patch('openlp.core.ui.firsttimewizard.QtWidgets.QWizard.reject')
     @patch('openlp.core.ui.firsttimeform.time')
     @patch('openlp.core.ui.firsttimeform.get_thread_worker')
     @patch('openlp.core.ui.firsttimeform.is_thread_finished')
-    def test_on_cancel_button_clicked(self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time):
+    def test_reject_method(
+            self, mocked_is_thread_finished, mocked_get_thread_worker, mocked_time, mocked_qwizard_reject):
         """
-        Test that the cancel button click slot shuts down the threads correctly
+        Test that the reject method shuts down the threads correctly
         """
         # GIVEN: A FRW, some mocked threads and workers (that isn't quite done) and other mocked stuff
         mocked_worker = MagicMock()
@@ -235,17 +305,47 @@
         frw.thumbnail_download_threads = ['test_thread']
         with patch.object(frw.application, 'set_normal_cursor') as mocked_set_normal_cursor:
 
-            # WHEN: on_cancel_button_clicked() is called
-            frw.on_cancel_button_clicked()
+            # WHEN: the reject method is called
+            frw.reject()
 
             # THEN: The right things should be called in the right order
-            assert frw.was_cancelled is True, 'The was_cancelled property should have been set to True'
             mocked_get_thread_worker.assert_called_once_with('test_thread')
             mocked_worker.cancel_download.assert_called_once()
             mocked_is_thread_finished.assert_called_with('test_thread')
             assert mocked_is_thread_finished.call_count == 2, 'isRunning() should have been called twice'
             mocked_time.sleep.assert_called_once_with(0.1)
-            mocked_set_normal_cursor.assert_called_once_with()
+            mocked_set_normal_cursor.assert_called_once()
+            mocked_qwizard_reject.assert_called_once()
+
+    @patch('openlp.core.ui.firsttimeform.ProxyDialog')
+    def test_on_custom_button_clicked(self, mocked_proxy_dialog):
+        """
+        Test _on_custom_button when it is called whe the 'internet settings' (CustomButton1) button is not clicked.
+        """
+        # GIVEN: An instance of the FirstTimeForm
+        frw = FirstTimeForm(None)
+
+        # WHEN: Calling _on_custom_button_clicked with a different button to the 'internet settings button.
+        frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton2)
+
+        # THEN: The ProxyDialog should not be shown.
+        mocked_proxy_dialog.assert_not_called()
+
+    @patch('openlp.core.ui.firsttimeform.ProxyDialog')
+    def test_on_custom_button_clicked_internet_settings(self, mocked_proxy_dialog):
+        """
+        Test _on_custom_button when it is called when the 'internet settings' (CustomButton1) button is clicked.
+        """
+        # GIVEN: An instance of the FirstTimeForm
+        frw = FirstTimeForm(None)
+
+        # WHEN: Calling _on_custom_button_clicked with the constant for the 'internet settings' button (CustomButton1)
+        frw._on_custom_button_clicked(QtWidgets.QWizard.CustomButton1)
+
+        # THEN: The ProxyDialog should be shown.
+        mocked_proxy_dialog.assert_called_with(frw)
+        mocked_proxy_dialog().retranslate_ui.assert_called_once()
+        mocked_proxy_dialog().exec.assert_called_once()
 
     @patch('openlp.core.ui.firsttimeform.critical_error_message_box')
     def test__parse_config_invalid_config(self, mocked_critical_error_message_box):


Follow ups