← Back to team overview

openlp-core team mailing list archive

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

 

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

Commit message:
Implement a proxy configuration widget. This is just a start other tasks that still need completing (to follow) are changing the web bibles over to use this, and allow setting up of proxy from FTW (else the sample download can fail)

lp:~phill-ridout/openlp/proxies (revision )
https://ci.openlp.io/job/Branch-01-Pull/2521/                          [WAITING]
[RUNNING]
[SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2421/                  [WAITING]
[RUNNING]
[SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/208/                   [WAITING]
[FAILURE]
Stopping after failure
https://ci.openlp.io/job/Branch-03a-Build-Source/116/                  [WAITING]
[SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/109/                   [WAITING]
[RUNNING]
[SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Analysis/1578/                [WAITING]
[SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1391/                [WAITING]
[SUCCESS]

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~phill-ridout/openlp/proxies/+merge/347673
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~phill-ridout/openlp/proxies into lp:openlp.
=== modified file 'openlp/core/common/httputils.py'
--- openlp/core/common/httputils.py	2018-01-04 06:10:20 +0000
+++ openlp/core/common/httputils.py	2018-06-08 07:01:59 +0000
@@ -32,6 +32,7 @@
 
 from openlp.core.common import trace_error_handler
 from openlp.core.common.registry import Registry
+from openlp.core.common.settings import ProxyMode, Settings
 
 log = logging.getLogger(__name__ + '.__init__')
 
@@ -64,6 +65,39 @@
 CONNECTION_RETRIES = 2
 
 
+def get_proxy_settings(mode=None):
+    """
+    Create a dictionary containing the proxy settings.
+
+    :param ProxyMode | None mode: Specify the source of the proxy settings
+    :return: A dict using the format expected by the requests library.
+    :rtype: dict | None
+    """
+    settings = Settings()
+    if mode is None:
+        mode = settings.value('advanced/proxy mode')
+    if mode == ProxyMode.NO_PROXY:
+        return {'http': None, 'https': None}
+    elif mode == ProxyMode.SYSTEM_PROXY:
+        # The requests library defaults to using the proxy settings in the environment variables
+        return
+    elif mode == ProxyMode.MANUAL_PROXY:
+        http_addr = settings.value('advanced/proxy http')
+        https_addr = settings.value('advanced/proxy https')
+        username = settings.value('advanced/proxy username')
+        password = settings.value('advanced/proxy password')
+        basic_auth = ''
+        if username:
+            basic_auth = '{username}:{password}@'.format(username=username, password=password)
+        http_value = None
+        https_value = None
+        if http_addr:
+            http_value = 'http://{basic_auth}{http_addr}'.format(basic_auth=basic_auth, http_addr=http_addr)
+        if https_addr:
+            https_value = 'https://{basic_auth}{https_addr}'.format(basic_auth=basic_auth, https_addr=https_addr)
+        return {'http': http_value, 'https': https_value}
+
+
 def get_user_agent():
     """
     Return a user agent customised for the platform the user is on.
@@ -75,7 +109,7 @@
     return browser_list[random_index]
 
 
-def get_web_page(url, headers=None, update_openlp=False, proxies=None):
+def get_web_page(url, headers=None, update_openlp=False, proxy_mode=None):
     """
     Attempts to download the webpage at url and returns that page or None.
 
@@ -90,6 +124,8 @@
         headers = {}
     if 'user-agent' not in [key.lower() for key in headers.keys()]:
         headers['User-Agent'] = get_user_agent()
+    if proxy_mode is None:
+        proxies = get_proxy_settings(mode=proxy_mode)
     log.debug('Downloading URL = %s' % url)
     retries = 0
     while retries < CONNECTION_RETRIES:

=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2018-05-03 14:58:50 +0000
+++ openlp/core/common/settings.py	2018-06-08 07:01:59 +0000
@@ -26,6 +26,7 @@
 import json
 import logging
 import os
+from enum import IntEnum
 from tempfile import gettempdir
 
 from PyQt5 import QtCore, QtGui
@@ -38,6 +39,13 @@
 
 __version__ = 2
 
+
+class ProxyMode(IntEnum):
+    NO_PROXY = 1
+    SYSTEM_PROXY = 2
+    MANUAL_PROXY = 3
+
+
 # Fix for bug #1014422.
 X11_BYPASS_DEFAULT = True
 if is_linux():                                                                              # pragma: no cover
@@ -116,6 +124,11 @@
         'advanced/print file meta data': False,
         'advanced/print notes': False,
         'advanced/print slide text': False,
+        'advanced/proxy mode': ProxyMode.SYSTEM_PROXY,
+        'advanced/proxy http': '',
+        'advanced/proxy https': '',
+        'advanced/proxy username': '',
+        'advanced/proxy password': '',
         'advanced/recent file count': 4,
         'advanced/save current plugin': False,
         'advanced/slide limits': SlideLimits.End,

=== modified file 'openlp/core/ui/advancedtab.py'
--- openlp/core/ui/advancedtab.py	2017-12-29 09:15:48 +0000
+++ openlp/core/ui/advancedtab.py	2018-06-08 07:01:59 +0000
@@ -35,6 +35,7 @@
 from openlp.core.ui.style import HAS_DARK_STYLE
 from openlp.core.widgets.edits import PathEdit
 from openlp.core.widgets.enums import PathEditType
+from openlp.core.widgets.widgets import ProxyWidget
 
 log = logging.getLogger(__name__)
 
@@ -76,6 +77,9 @@
         self.media_plugin_check_box = QtWidgets.QCheckBox(self.ui_group_box)
         self.media_plugin_check_box.setObjectName('media_plugin_check_box')
         self.ui_layout.addRow(self.media_plugin_check_box)
+        self.hide_mouse_check_box = QtWidgets.QCheckBox(self.ui_group_box)
+        self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
+        self.ui_layout.addWidget(self.hide_mouse_check_box)
         self.double_click_live_check_box = QtWidgets.QCheckBox(self.ui_group_box)
         self.double_click_live_check_box.setObjectName('double_click_live_check_box')
         self.ui_layout.addRow(self.double_click_live_check_box)
@@ -116,6 +120,24 @@
             self.use_dark_style_checkbox = QtWidgets.QCheckBox(self.ui_group_box)
             self.use_dark_style_checkbox.setObjectName('use_dark_style_checkbox')
             self.ui_layout.addRow(self.use_dark_style_checkbox)
+        # Service Item Slide Limits
+        self.slide_group_box = QtWidgets.QGroupBox(self.left_column)
+        self.slide_group_box.setObjectName('slide_group_box')
+        self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
+        self.slide_layout.setObjectName('slide_layout')
+        self.slide_label = QtWidgets.QLabel(self.slide_group_box)
+        self.slide_label.setWordWrap(True)
+        self.slide_layout.addWidget(self.slide_label)
+        self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
+        self.end_slide_radio_button.setObjectName('end_slide_radio_button')
+        self.slide_layout.addWidget(self.end_slide_radio_button)
+        self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
+        self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
+        self.slide_layout.addWidget(self.wrap_slide_radio_button)
+        self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
+        self.next_item_radio_button.setObjectName('next_item_radio_button')
+        self.slide_layout.addWidget(self.next_item_radio_button)
+        self.left_layout.addWidget(self.slide_group_box)
         # Data Directory
         self.data_directory_group_box = QtWidgets.QGroupBox(self.left_column)
         self.data_directory_group_box.setObjectName('data_directory_group_box')
@@ -142,33 +164,6 @@
         self.data_directory_layout.addRow(self.data_directory_copy_check_layout)
         self.data_directory_layout.addRow(self.new_data_directory_has_files_label)
         self.left_layout.addWidget(self.data_directory_group_box)
-        # Hide mouse
-        self.hide_mouse_group_box = QtWidgets.QGroupBox(self.right_column)
-        self.hide_mouse_group_box.setObjectName('hide_mouse_group_box')
-        self.hide_mouse_layout = QtWidgets.QVBoxLayout(self.hide_mouse_group_box)
-        self.hide_mouse_layout.setObjectName('hide_mouse_layout')
-        self.hide_mouse_check_box = QtWidgets.QCheckBox(self.hide_mouse_group_box)
-        self.hide_mouse_check_box.setObjectName('hide_mouse_check_box')
-        self.hide_mouse_layout.addWidget(self.hide_mouse_check_box)
-        self.right_layout.addWidget(self.hide_mouse_group_box)
-        # Service Item Slide Limits
-        self.slide_group_box = QtWidgets.QGroupBox(self.right_column)
-        self.slide_group_box.setObjectName('slide_group_box')
-        self.slide_layout = QtWidgets.QVBoxLayout(self.slide_group_box)
-        self.slide_layout.setObjectName('slide_layout')
-        self.slide_label = QtWidgets.QLabel(self.slide_group_box)
-        self.slide_label.setWordWrap(True)
-        self.slide_layout.addWidget(self.slide_label)
-        self.end_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
-        self.end_slide_radio_button.setObjectName('end_slide_radio_button')
-        self.slide_layout.addWidget(self.end_slide_radio_button)
-        self.wrap_slide_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
-        self.wrap_slide_radio_button.setObjectName('wrap_slide_radio_button')
-        self.slide_layout.addWidget(self.wrap_slide_radio_button)
-        self.next_item_radio_button = QtWidgets.QRadioButton(self.slide_group_box)
-        self.next_item_radio_button.setObjectName('next_item_radio_button')
-        self.slide_layout.addWidget(self.next_item_radio_button)
-        self.right_layout.addWidget(self.slide_group_box)
         # Display Workarounds
         self.display_workaround_group_box = QtWidgets.QGroupBox(self.right_column)
         self.display_workaround_group_box.setObjectName('display_workaround_group_box')
@@ -223,6 +218,9 @@
         self.service_name_example.setObjectName('service_name_example')
         self.service_name_layout.addRow(self.service_name_example_label, self.service_name_example)
         self.right_layout.addWidget(self.service_name_group_box)
+        # Proxies
+        self.proxy_widget = ProxyWidget(self.right_column)
+        self.right_layout.addWidget(self.proxy_widget)
         # After the last item on each side, add some spacing
         self.left_layout.addStretch()
         self.right_layout.addStretch()
@@ -311,7 +309,6 @@
             translate('OpenLP.AdvancedTab',
                       'Revert to the default service name "{name}".').format(name=UiStrings().DefaultServiceName))
         self.service_name_example_label.setText(translate('OpenLP.AdvancedTab', 'Example:'))
-        self.hide_mouse_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Mouse Cursor'))
         self.hide_mouse_check_box.setText(translate('OpenLP.AdvancedTab', 'Hide mouse cursor when over display window'))
         self.data_directory_new_label.setText(translate('OpenLP.AdvancedTab', 'Path:'))
         self.data_directory_cancel_button.setText(translate('OpenLP.AdvancedTab', 'Cancel'))
@@ -334,6 +331,7 @@
         self.wrap_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Wrap around'))
         self.next_item_radio_button.setText(translate('OpenLP.GeneralTab', '&Move to next/previous service item'))
         self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
+        self.proxy_widget.retranslate_ui()
 
     def load(self):
         """
@@ -436,6 +434,7 @@
         if HAS_DARK_STYLE:
             settings.setValue('use_dark_style', self.use_dark_style_checkbox.isChecked())
         settings.endGroup()
+        self.proxy_widget.save()
 
     def on_search_as_type_check_box_changed(self, check_state):
         self.is_search_as_you_type_enabled = (check_state == QtCore.Qt.Checked)

=== added file 'openlp/core/widgets/widgets.py'
--- openlp/core/widgets/widgets.py	1970-01-01 00:00:00 +0000
+++ openlp/core/widgets/widgets.py	2018-06-08 07:01:59 +0000
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2018 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.widgets.widgets` module contains custom widgets used in OpenLP
+"""
+from PyQt5 import QtWidgets
+
+from openlp.core.common.i18n import translate
+from openlp.core.common.settings import ProxyMode, Settings
+
+
+class ProxyWidget(QtWidgets.QGroupBox):
+    """
+    A proxy settings widget that implements loading and saving its settings.
+    """
+    def __init__(self, parent=None):
+        """
+        Initialise the widget.
+
+        :param QtWidgets.QWidget | None parent: The widgets parent
+        """
+        super().__init__(parent)
+        self._setup()
+
+    def _setup(self):
+        """
+        A setup method seperate from __init__ to allow easier testing
+        """
+        self.setup_ui()
+        self.load()
+
+    def setup_ui(self):
+        """
+        Create the widget layout and sub widgets
+        """
+        self.layout = QtWidgets.QFormLayout(self)
+        self.radio_group = QtWidgets.QButtonGroup(self)
+        self.no_proxy_radio = QtWidgets.QRadioButton('', self)
+        self.radio_group.addButton(self.no_proxy_radio, ProxyMode.NO_PROXY)
+        self.layout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.no_proxy_radio)
+        self.use_sysem_proxy_radio = QtWidgets.QRadioButton('', self)
+        self.radio_group.addButton(self.use_sysem_proxy_radio, ProxyMode.SYSTEM_PROXY)
+        self.layout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.use_sysem_proxy_radio)
+        self.manual_proxy_radio = QtWidgets.QRadioButton('', self)
+        self.radio_group.addButton(self.manual_proxy_radio, ProxyMode.MANUAL_PROXY)
+        self.layout.setWidget(2, QtWidgets.QFormLayout.SpanningRole, self.manual_proxy_radio)
+        self.http_edit = QtWidgets.QLineEdit(self)
+        self.layout.addRow('HTTP:', self.http_edit)
+        self.https_edit = QtWidgets.QLineEdit(self)
+        self.layout.addRow('HTTPS:', self.https_edit)
+        self.username_edit = QtWidgets.QLineEdit(self)
+        self.layout.addRow('Username:', self.username_edit)
+        self.password_edit = QtWidgets.QLineEdit(self)
+        self.password_edit.setEchoMode(QtWidgets.QLineEdit.Password)
+        self.layout.addRow('Password:', self.password_edit)
+        # Signal / Slots
+        self.radio_group.buttonToggled.connect(self.on_radio_group_button_toggled)
+
+    # @QtCore.pyqtSlot(int, bool)  For some reason PyQt doesn't think this signature exists.
+    #                              (It does according to the docs)
+    def on_radio_group_button_toggled(self, button, checked):
+        """
+        Handles the toggled signal on the radio buttons. The signal is emitted twice if a radio butting being toggled on
+        causes another radio button in the group to be toggled off.
+
+        En/Disables the `Manual Proxy` line edits depending on the currently selected radio button
+
+        :param QtWidgets.QRadioButton button: The button that has toggled
+        :param bool checked: The buttons new state
+        """
+        id = self.radio_group.id(button)  # The work around (see above comment)
+        enable_manual_edits = id == ProxyMode.MANUAL_PROXY and checked
+        self.http_edit.setEnabled(enable_manual_edits)
+        self.https_edit.setEnabled(enable_manual_edits)
+        self.username_edit.setEnabled(enable_manual_edits)
+        self.password_edit.setEnabled(enable_manual_edits)
+
+    def retranslate_ui(self):
+        """
+        Translate the Ui
+        """
+        self.setTitle(translate('OpenLP.ProxyWidget', 'Proxy Server Settings'))
+        self.no_proxy_radio.setText(translate('OpenLP.ProxyWidget', 'No prox&y'))
+        self.use_sysem_proxy_radio.setText(translate('OpenLP.ProxyWidget', '&Use system proxy'))
+        self.manual_proxy_radio.setText(translate('OpenLP.ProxyWidget', '&Manual proxy configuration'))
+        proxy_example = translate('OpenLP.ProxyWidget', 'e.g. proxy_server_address:port_no')
+        self.layout.labelForField(self.http_edit).setText('HTTP:')
+        self.http_edit.setPlaceholderText(proxy_example)
+        self.layout.labelForField(self.https_edit).setText('HTTPS:')
+        self.https_edit.setPlaceholderText(proxy_example)
+        self.layout.labelForField(self.username_edit).setText('Username:')
+        self.layout.labelForField(self.password_edit).setText('Password:')
+
+    def load(self):
+        """
+        Load the data from the settings to the widget.
+        """
+        settings = Settings()
+        checked_radio = self.radio_group.button(settings.value('advanced/proxy mode'))
+        checked_radio.setChecked(True)
+        self.http_edit.setText(settings.value('advanced/proxy http'))
+        self.https_edit.setText(settings.value('advanced/proxy https'))
+        self.username_edit.setText(settings.value('advanced/proxy username'))
+        self.password_edit.setText(settings.value('advanced/proxy password'))
+
+    def save(self):
+        """
+        Save the widget data to the settings
+        """
+        settings = Settings()  # TODO: Migrate from old system
+        settings.setValue('advanced/proxy mode', self.radio_group.checkedId())
+        settings.setValue('advanced/proxy http', self.http_edit.text())
+        settings.setValue('advanced/proxy https', self.https_edit.text())
+        settings.setValue('advanced/proxy username', self.username_edit.text())
+        settings.setValue('advanced/proxy password', self.password_edit.text())

=== modified file 'scripts/jenkins_script.py'
--- scripts/jenkins_script.py	2017-12-29 09:52:58 +0000
+++ scripts/jenkins_script.py	2018-06-08 07:01:59 +0000
@@ -199,6 +199,7 @@
     """
     This returns the name of branch of the working directory. For example it returns *lp:~googol/openlp/render*.
     """
+    return 'lp:~phill-ridout/openlp/proxies'
     # Run the bzr command.
     bzr = Popen(('bzr', 'info'), stdout=PIPE, stderr=PIPE)
     raw_output, error = bzr.communicate()

=== modified file 'scripts/lp-merge.py'
--- scripts/lp-merge.py	2018-05-04 21:14:04 +0000
+++ scripts/lp-merge.py	2018-06-08 07:01:59 +0000
@@ -104,7 +104,7 @@
     # Find the p tag that contains the commit message
     # <div id="commit-message">...<div id="edit-commit_message">...<div class="yui3-editable_text-text"><p>
     commit_message = soup.find('div', id='commit-message').find('div', id='edit-commit_message')\
-            .find('div', 'yui3-editable_text-text').p
+        .find('div', 'yui3-editable_text-text').p
     merge_info['commit_message'] = commit_message.string
     # Find all tr-tags with this class. Makes it possible to get bug numbers.
     # <tr class="bug-branch-summary"

=== modified file 'tests/functional/openlp_core/common/test_httputils.py'
--- tests/functional/openlp_core/common/test_httputils.py	2018-01-04 06:10:20 +0000
+++ tests/functional/openlp_core/common/test_httputils.py	2018-06-08 07:01:59 +0000
@@ -27,13 +27,14 @@
 from unittest import TestCase
 from unittest.mock import MagicMock, patch
 
-from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, download_file
+from openlp.core.common.httputils import ProxyMode, download_file, get_proxy_settings, get_url_file_size, \
+    get_user_agent, get_web_page
 from openlp.core.common.path import Path
+from openlp.core.common.settings import Settings
 from tests.helpers.testmixin import TestMixin
 
 
 class TestHttpUtils(TestCase, TestMixin):
-
     """
     A test suite to test out various http helper functions.
     """
@@ -240,3 +241,119 @@
         # 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
         assert os.path.exists(self.tempfile) is False, 'tempfile should have been deleted'
+
+
+class TestGetProxySettings(TestCase, TestMixin):
+    def setUp(self):
+        self.build_settings()
+        self.addCleanup(self.destroy_settings)
+
+    @patch('openlp.core.common.httputils.Settings')
+    def test_mode_arg_specified(self, MockSettings):
+        """
+        Test that the argument is used rather than reading the 'advanced/proxy mode' setting
+        """
+        # GIVEN: Mocked settings
+        mocked_settings = MagicMock()
+        MockSettings.return_value = mocked_settings
+
+        # WHEN: Calling `get_proxy_settings` with the mode arg specified
+        get_proxy_settings(mode=ProxyMode.NO_PROXY)
+
+        # THEN: The mode arg should have been used rather than looking it up in the settings
+        mocked_settings.value.assert_not_called()
+
+    @patch('openlp.core.common.httputils.Settings')
+    def test_mode_incorrect_arg_specified(self, MockSettings):
+        """
+        Test that the system settings are used when the mode arg specieied is invalid
+        """
+        # GIVEN: Mocked settings
+        mocked_settings = MagicMock()
+        MockSettings.return_value = mocked_settings
+
+        # WHEN: Calling `get_proxy_settings` with an invalid mode arg specified
+        result = get_proxy_settings(mode='qwerty')
+
+        # THEN: An None should be returned
+        mocked_settings.value.assert_not_called()
+        assert result is None
+
+    def test_no_proxy_mode(self):
+        """
+        Test that a dictionary with http and https values are set to None is returned, when `NO_PROXY` mode is specified
+        """
+        # GIVEN: A `proxy mode` setting of NO_PROXY
+        Settings().setValue('advanced/proxy mode', ProxyMode.NO_PROXY)
+
+        # WHEN: Calling `get_proxy_settings`
+        result = get_proxy_settings()
+
+        # THEN: The returned value should be a dictionary with http and https values set to None
+        assert result == {'http': None, 'https': None}
+
+    def test_system_proxy_mode(self):
+        """
+        Test that None is returned, when `SYSTEM_PROXY` mode is specified
+        """
+        # GIVEN: A `proxy mode` setting of SYSTEM_PROXY
+        Settings().setValue('advanced/proxy mode', ProxyMode.SYSTEM_PROXY)
+
+        # WHEN: Calling `get_proxy_settings`
+        result = get_proxy_settings()
+
+        # THEN: The returned value should be None
+        assert result is None
+
+    def test_manual_proxy_mode_no_auth(self):
+        """
+        Test that the correct proxy addresses are returned when basic authentication is not used
+        """
+        # GIVEN: A `proxy mode` setting of MANUAL_PROXY with proxy servers, but no auth credentials are supplied
+        Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
+        Settings().setValue('advanced/proxy http', 'testhttp.server:port')
+        Settings().setValue('advanced/proxy https', 'testhttps.server:port')
+        Settings().setValue('advanced/proxy username', '')
+        Settings().setValue('advanced/proxy password', '')
+
+        # WHEN: Calling `get_proxy_settings`
+        result = get_proxy_settings()
+
+        # THEN: The returned value should be the proxy servers without authentication
+        assert result == {'http': 'http://testhttp.server:port', 'https': 'https://testhttps.server:port'}
+
+    def test_manual_proxy_mode_auth(self):
+        """
+        Test that the correct proxy addresses are returned when basic authentication is used
+        """
+        # GIVEN: A `proxy mode` setting of MANUAL_PROXY with proxy servers and auth credentials supplied
+        Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
+        Settings().setValue('advanced/proxy http', 'testhttp.server:port')
+        Settings().setValue('advanced/proxy https', 'testhttps.server:port')
+        Settings().setValue('advanced/proxy username', 'user')
+        Settings().setValue('advanced/proxy password', 'pass')
+
+        # WHEN: Calling `get_proxy_settings`
+        result = get_proxy_settings()
+
+        # THEN: The returned value should be the proxy servers with the authentication credentials
+        assert result == {'http': 'http://user:pass@testhttp.server:port',
+                          'https': 'https://user:pass@testhttps.server:port'}
+
+    def test_manual_proxy_mode_no_servers(self):
+        """
+        Test that the system proxies are overidden when the MANUAL_PROXY mode is specified, but no server addresses are
+        supplied
+        """
+        # GIVEN: A `proxy mode` setting of MANUAL_PROXY with no servers specified
+        Settings().setValue('advanced/proxy mode', ProxyMode.MANUAL_PROXY)
+        Settings().setValue('advanced/proxy http', '')
+        Settings().setValue('advanced/proxy https', '')
+        Settings().setValue('advanced/proxy username', 'user')
+        Settings().setValue('advanced/proxy password', 'pass')
+
+        # WHEN: Calling `get_proxy_settings`
+        result = get_proxy_settings()
+
+        # THEN: The returned value should be the proxy servers set to None
+        assert result == {'http': None, 'https': None}

=== added file 'tests/interfaces/openlp_core/widgets/test_widgets.py'
--- tests/interfaces/openlp_core/widgets/test_widgets.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_core/widgets/test_widgets.py	2018-06-08 07:01:59 +0000
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2018 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                          #
+###############################################################################
+"""
+Module to test the custom widgets.
+"""
+from unittest import TestCase
+from unittest.mock import MagicMock, call, patch
+
+from openlp.core.common.registry import Registry
+from openlp.core.common.settings import ProxyMode
+from openlp.core.widgets.widgets import ProxyWidget
+from tests.helpers.testmixin import TestMixin
+
+
+class TestProxyWidget(TestCase, TestMixin):
+    """
+    Test the EditCustomForm.
+    """
+    def setUp(self):
+        """
+        Create the UI
+        """
+        Registry.create()
+        self.setup_application()
+
+    def test_radio_button_exclusivity(self):
+        """
+        Test that only one radio button can be checked at a time, and that the line edits are only enabled when the
+        `manual_proxy_radio` is checked
+        """
+        # GIVEN: An instance of the `openlp.core.common.widgets.widgets.ProxyWidget`
+        proxy_widget = ProxyWidget()
+
+        # WHEN: 'Checking' the `no_proxy_radio` button
+        proxy_widget.no_proxy_radio.setChecked(True)
+
+        # THEN: The other radio buttons should not be checked and the line edits should not be enabled
+        assert proxy_widget.use_sysem_proxy_radio.isChecked() is False
+        assert proxy_widget.manual_proxy_radio.isChecked() is False
+        assert proxy_widget.http_edit.isEnabled() is False
+        assert proxy_widget.https_edit.isEnabled() is False
+        assert proxy_widget.username_edit.isEnabled() is False
+        assert proxy_widget.password_edit.isEnabled() is False
+
+        # WHEN: 'Checking' the `use_sysem_proxy_radio` button
+        proxy_widget.use_sysem_proxy_radio.setChecked(True)
+
+        # THEN: The other radio buttons should not be checked and the line edits should not be enabled
+        assert proxy_widget.no_proxy_radio.isChecked() is False
+        assert proxy_widget.manual_proxy_radio.isChecked() is False
+        assert proxy_widget.http_edit.isEnabled() is False
+        assert proxy_widget.https_edit.isEnabled() is False
+        assert proxy_widget.username_edit.isEnabled() is False
+        assert proxy_widget.password_edit.isEnabled() is False
+
+        # WHEN: 'Checking' the `manual_proxy_radio` button
+        proxy_widget.manual_proxy_radio.setChecked(True)
+
+        # THEN: The other radio buttons should not be checked and the line edits should be enabled
+        assert proxy_widget.no_proxy_radio.isChecked() is False
+        assert proxy_widget.use_sysem_proxy_radio.isChecked() is False
+        assert proxy_widget.http_edit.isEnabled() is True
+        assert proxy_widget.https_edit.isEnabled() is True
+        assert proxy_widget.username_edit.isEnabled() is True
+        assert proxy_widget.password_edit.isEnabled() is True
+
+    def test_proxy_widget_load_default_settings(self):
+        """
+        Test that the default settings are loaded from the config correctly
+        """
+        # GIVEN: And instance of the widget with default settings
+        proxy_widget = ProxyWidget()
+
+        # WHEN: Calling the `load` method
+        proxy_widget.load()
+
+        # THEN: The widget should be in its default state
+        assert proxy_widget.use_sysem_proxy_radio.isChecked() is True
+        assert proxy_widget.http_edit.text() == ''
+        assert proxy_widget.https_edit.text() == ''
+        assert proxy_widget.username_edit.text() == ''
+        assert proxy_widget.password_edit.text() == ''
+
+    @patch.object(ProxyWidget, 'load')
+    @patch('openlp.core.widgets.widgets.Settings')
+    def test_proxy_widget_save_no_proxy_settings(self, settings_patcher, proxy_widget_load_patcher):
+        """
+        Test that the settings are saved correctly
+        """
+        # GIVEN: A Mocked settings instance of the proxy widget with some known values set
+        settings_instance = MagicMock()
+        settings_patcher.return_value = settings_instance
+        proxy_widget = ProxyWidget()
+        proxy_widget.no_proxy_radio.setChecked(True)
+        proxy_widget.http_edit.setText('')
+        proxy_widget.https_edit.setText('')
+        proxy_widget.username_edit.setText('')
+        proxy_widget.password_edit.setText('')
+
+        # WHEN: Calling save
+        proxy_widget.save()
+
+        # THEN: The settings should be set as expected
+        settings_instance.setValue.assert_has_calls(
+            [call('advanced/proxy mode', ProxyMode.NO_PROXY),
+             call('advanced/proxy http', ''),
+             call('advanced/proxy https', ''),
+             call('advanced/proxy username', ''),
+             call('advanced/proxy password', '')])
+
+    @patch.object(ProxyWidget, 'load')
+    @patch('openlp.core.widgets.widgets.Settings')
+    def test_proxy_widget_save_manual_settings(self, settings_patcher, proxy_widget_load_patcher):
+        """
+        Test that the settings are saved correctly
+        """
+        # GIVEN: A Mocked and instance of the proxy widget with some known values set
+        settings_instance = MagicMock()
+        settings_patcher.return_value = settings_instance
+        proxy_widget = ProxyWidget()
+        proxy_widget.manual_proxy_radio.setChecked(True)
+        proxy_widget.http_edit.setText('http_proxy_server:port')
+        proxy_widget.https_edit.setText('https_proxy_server:port')
+        proxy_widget.username_edit.setText('username')
+        proxy_widget.password_edit.setText('password')
+
+        # WHEN: Calling save
+        proxy_widget.save()
+
+        # THEN: The settings should be set as expected
+        settings_instance.setValue.assert_has_calls(
+            [call('advanced/proxy mode', ProxyMode.MANUAL_PROXY),
+             call('advanced/proxy http', 'http_proxy_server:port'),
+             call('advanced/proxy https', 'https_proxy_server:port'),
+             call('advanced/proxy username', 'username'),
+             call('advanced/proxy password', 'password')])


Follow ups