=== modified file 'bin/ubuntu-sso-login-qt'
--- bin/ubuntu-sso-login-qt 2012-02-13 15:43:59 +0000
+++ bin/ubuntu-sso-login-qt 2012-03-20 16:10:09 +0000
@@ -15,16 +15,19 @@
# You should have received a copy of the GNU General Public License along
# with this program. If not, see .
-"""Start the sso GTK UI."""
+"""Start the sso Qt UI."""
# Invalid name "ubuntu-sso-login-qt", pylint: disable=C0103
# Access to a protected member, pylint: disable=W0212
+import sys
+
from ubuntu_sso.qt.main import main
from ubuntu_sso.utils.ui import parse_args
-from dbus.mainloop.qt import DBusQtMainLoop
-DBusQtMainLoop(set_as_default=True)
+if sys.platform.startswith('linux'):
+ from dbus.mainloop.qt import DBusQtMainLoop
+ DBusQtMainLoop(set_as_default=True)
if __name__ == "__main__":
=== modified file 'bin/ubuntu-sso-proxy-creds-qt'
--- bin/ubuntu-sso-proxy-creds-qt 2012-02-24 13:07:06 +0000
+++ bin/ubuntu-sso-proxy-creds-qt 2012-03-20 16:10:09 +0000
@@ -19,9 +19,12 @@
# Invalid name, pylint: disable=C0103
-# set the dbus main loop to be used
-from dbus.mainloop.qt import DBusQtMainLoop
-DBusQtMainLoop(set_as_default=True)
+import sys
+
+if sys.platform.startswith('linux'):
+ # set the dbus main loop to be used
+ from dbus.mainloop.qt import DBusQtMainLoop
+ DBusQtMainLoop(set_as_default=True)
from ubuntu_sso.qt.proxy_dialog import main
=== added file 'data/qt/linux.qss'
--- data/qt/linux.qss 1970-01-01 00:00:00 +0000
+++ data/qt/linux.qss 2012-03-20 16:10:09 +0000
@@ -0,0 +1,2 @@
+/* Styles specific to the linux platform */
+
=== modified file 'data/qt/loadingoverlay.ui'
--- data/qt/loadingoverlay.ui 2012-02-22 16:58:08 +0000
+++ data/qt/loadingoverlay.ui 2012-03-20 16:10:09 +0000
@@ -52,11 +52,6 @@
0
-
-
- 14
-
-
Getting information, please wait...
=== modified file 'data/qt/proxy_credentials_dialog.ui'
--- data/qt/proxy_credentials_dialog.ui 2012-02-23 11:47:00 +0000
+++ data/qt/proxy_credentials_dialog.ui 2012-03-20 16:10:09 +0000
@@ -113,8 +113,6 @@
- 14
- 75
true
=== modified file 'data/qt/reset_password.ui'
--- data/qt/reset_password.ui 2012-03-05 20:30:57 +0000
+++ data/qt/reset_password.ui 2012-03-20 16:10:09 +0000
@@ -6,14 +6,14 @@
0
0
- 544
- 280
+ 505
+ 260
-
- Qt::LeftToRight
+
+
-
+
15
@@ -21,261 +21,215 @@
0
-
-
-
- 0
-
-
-
-
-
- 15
-
-
-
-
-
- 3
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 310
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
-
- 75
- true
-
-
-
- reset_code
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 300
- 0
-
-
-
-
- 300
- 16777215
-
-
-
-
-
-
- -
-
-
- 3
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 310
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
-
- 75
- true
-
-
-
- password_label
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 300
- 0
-
-
-
-
- 300
- 16777215
-
-
-
- QLineEdit::Password
-
-
-
-
-
- -
-
-
- 3
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 310
- 0
-
-
-
-
- 75
- true
-
-
-
- confirm_password_label
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 300
- 0
-
-
-
-
- 300
- 16777215
-
-
-
- QLineEdit::Password
-
-
-
-
-
-
-
- -
+
+
-
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 310
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+
+ true
+
+
+
+ reset_code
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 0
+
+
+
+
+ 300
+ 16777215
+
+
+
+
+
+
+ -
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 310
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+
+ true
+
+
+
+ password_label
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 0
+
+
+
+
+ 300
+ 16777215
+
+
+
+ QLineEdit::Password
+
+
+
+
+
+ -
- 0
+ 3
-
-
-
-
- 0
- 0
-
-
-
-
- 220
- 100
+
+
+
+ 0
+ 0
+
+
+
+
+ 310
+ 0
+
+
+
+
+ true
+
+
+
+ confirm_password_label
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 300
+ 0
- 220
+ 300
16777215
-
- password_assistance
-
-
- 20
+
+ QLineEdit::Password
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Ignored
-
-
-
- 220
- 0
-
-
-
-
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 185
+ 95
+
+
+
+
+ 185
+ 95
+
+
+
+ password_assistance
+
+
+ true
+
+
+ 20
+
+
+
-
@@ -327,22 +281,5 @@
-
-
- confirm_password_line_edit
- returnPressed()
- reset_password_button
- click()
-
-
- 160
- 81
-
-
- 541
- 237
-
-
-
-
+
=== modified file 'data/qt/resources.qrc'
--- data/qt/resources.qrc 2012-02-16 14:13:36 +0000
+++ data/qt/resources.qrc 2012-03-20 16:10:09 +0000
@@ -6,5 +6,7 @@
../Ubuntu-B.ttf
../balloon_shape.png
stylesheet.qss
+ windows.qss
+ linux.qss
=== modified file 'data/qt/setup_account.ui'
--- data/qt/setup_account.ui 2012-03-05 21:52:45 +0000
+++ data/qt/setup_account.ui 2012-03-20 16:10:09 +0000
@@ -85,7 +85,6 @@
- 75
true
@@ -114,11 +113,6 @@
16777215
-
-
- 11
-
-
false
@@ -135,7 +129,6 @@
- 75
true
@@ -164,11 +157,6 @@
16777215
-
-
- 11
-
-
@@ -229,7 +217,6 @@
- 75
true
@@ -258,11 +245,6 @@
16777215
-
-
- 11
-
-
@@ -377,7 +359,6 @@
- 75
true
@@ -406,13 +387,8 @@
16777215
-
-
- 11
-
-
- Your password must be at least 8 characters long and at least contain one number and one upper later.
+ Your password must be at least 8 characters long and contain at least one number and one uppercase letter.
@@ -477,7 +453,6 @@
- 75
true
@@ -506,11 +481,6 @@
16777215
-
-
- 11
-
-
QLineEdit::Password
@@ -580,11 +550,6 @@
16777215
-
-
- 11
-
-
=== modified file 'data/qt/ssl_dialog.ui'
--- data/qt/ssl_dialog.ui 2012-02-24 15:22:26 +0000
+++ data/qt/ssl_dialog.ui 2012-03-20 16:10:09 +0000
@@ -95,11 +95,6 @@
-
-
-
- 14
-
-
Do you want to connect to this server
=== modified file 'data/qt/stylesheet.qss'
--- data/qt/stylesheet.qss 2012-03-05 20:30:57 +0000
+++ data/qt/stylesheet.qss 2012-03-20 16:10:09 +0000
@@ -1,5 +1,4 @@
QWidget {
- font-family: "Ubuntu";
color: #333333;
}
@@ -16,7 +15,6 @@
QLabel#password_assistance {
border-image: url(":/balloon_shape.png");
- font-size: 12px;
}
QLineEdit {
@@ -91,21 +89,11 @@
min-height: 100px;
}
-QFrame#frm_box > QLabel {
- font-size: 20px;
-}
-
-QLabel#title_label {
- font-size: 20px;
-}
-
-QFrame#header {
+WizardHeader {
padding-top: 1px;
padding-bottom: 1px;
}
QLabel#form_errors {
- font: bold 14px;
- color: #df2d1f;
padding-bottom: 1px;
}
=== added file 'data/qt/windows.qss'
--- data/qt/windows.qss 1970-01-01 00:00:00 +0000
+++ data/qt/windows.qss 2012-03-20 16:10:09 +0000
@@ -0,0 +1,5 @@
+/* Styles specific to the windows platform */
+
+QWidget {
+ font-family: "Ubuntu";
+}
=== modified file 'po/POTFILES.in'
--- po/POTFILES.in 2010-11-19 21:35:11 +0000
+++ po/POTFILES.in 2012-03-20 16:10:09 +0000
@@ -1,1 +1,2 @@
ubuntu_sso/gtk/gui.py
+ubuntu_sso/utils/ui.py
=== modified file 'run-tests'
--- run-tests 2012-02-17 16:57:34 +0000
+++ run-tests 2012-03-20 16:10:09 +0000
@@ -57,7 +57,7 @@
echo "*** Running QT test suite for ""$MODULE"" ***"
./setup.py build
-USE_QT_MAINLOOP=True $XVFB_CMDLINE u1trial --reactor=qt4 --gui -p "$GTK_TESTS_PATH" -i "test_windows.py" "$MODULE"
+$XVFB_CMDLINE u1trial --reactor=qt4 --gui -p "$GTK_TESTS_PATH" -i "test_windows.py" "$MODULE"
rm -rf _trial_temp
rm -rf build
=== modified file 'ubuntu_sso/__init__.py'
--- ubuntu_sso/__init__.py 2012-02-11 19:25:01 +0000
+++ ubuntu_sso/__init__.py 2012-03-20 16:10:09 +0000
@@ -15,6 +15,8 @@
# with this program. If not, see .
"""Ubuntu Single Sign On client code."""
+import sys
+
# DBus constants
DBUS_BUS_NAME = "com.ubuntu.sso"
@@ -29,7 +31,13 @@
# return codes for UIs
USER_SUCCESS = 0
USER_CANCELLATION = 10
+EXCEPTION_RAISED = 11
# available UIs
UI_EXECUTABLE_GTK = 'ubuntu-sso-login-gtk'
UI_EXECUTABLE_QT = 'ubuntu-sso-login-qt'
+UI_PROXY_CREDS_DIALOG = 'ubuntu-sso-proxy-creds-qt'
+
+if getattr(sys, "frozen", None) is not None and sys.platform == "win32":
+ UI_EXECUTABLE_QT += ".exe"
+ UI_PROXY_CREDS_DIALOG += ".exe"
=== modified file 'ubuntu_sso/gtk/gui.py'
--- ubuntu_sso/gtk/gui.py 2012-02-17 18:43:17 +0000
+++ ubuntu_sso/gtk/gui.py 2012-03-20 16:10:09 +0000
@@ -90,23 +90,6 @@
return c
# pylint: enable=C0103
-
-# To be removed when Python bindings provide these constants
-# as per http://code.google.com/p/pywebkitgtk/issues/detail?id=44
-# WebKitLoadStatus
-WEBKIT_LOAD_PROVISIONAL = 0
-WEBKIT_LOAD_COMMITTED = 1
-WEBKIT_LOAD_FINISHED = 2
-WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT = 3
-WEBKIT_LOAD_FAILED = 4
-# WebKitWebNavigationReason
-WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED = 0
-WEBKIT_WEB_NAVIGATION_REASON_FORM_SUBMITTED = 1
-WEBKIT_WEB_NAVIGATION_REASON_BACK_FORWARD = 2
-WEBKIT_WEB_NAVIGATION_REASON_RELOAD = 3
-WEBKIT_WEB_NAVIGATION_REASON_FORM_RESUBMITTED = 4
-WEBKIT_WEB_NAVIGATION_REASON_OTHER = 5
-
DEFAULT_WIDTH = 30
# To be replaced by values from the theme (LP: #616526)
HELP_TEXT_COLOR = parse_color("#bfbfbf")
@@ -947,23 +930,14 @@
self._set_current_page(self.processing_vbox)
- def on_tc_button_clicked(self, *args, **kwargs):
- """The T&C button was clicked, create the browser and load terms."""
+ def _add_webkit_browser(self):
+ """Add the webkit browser for the t&c."""
# delay the import of webkit to be able to build without it
from gi.repository import WebKit # pylint: disable=E0611
+
browser = WebKit.WebView()
- # The signal WebKitWebView::load-finished is deprecated and should not
- # be used in newly-written code. Use the "load-status" property
- # instead. Connect to "notify::load-status" to monitor loading.
-
- # nataliabidart (2010-10-04): connecting this signal makes the loading
- # of the Ubuntu One terms URL to fail. So we're using the deprecated
- # 'load-finished' for now.
-
- #browser.connect('notify::load-status',
- # self.on_tc_browser_notify_load_status)
- browser.connect('load-finished',
+ browser.connect('notify::load-status',
self.on_tc_browser_notify_load_status)
browser.connect('navigation-policy-decision-requested',
self.on_tc_browser_navigation_requested)
@@ -978,7 +952,14 @@
browser.load_uri(self.tc_url)
browser.show()
self.tc_browser_window.add(browser)
- self._set_current_page(self.processing_vbox)
+
+ def on_tc_button_clicked(self, *args, **kwargs):
+ """The T&C button was clicked, create the browser and load terms."""
+ if self.tc_browser_window.get_child() is None:
+ self._add_webkit_browser()
+ self._set_current_page(self.processing_vbox)
+ else:
+ self._set_current_page(self.tc_browser_vbox)
def on_tc_back_button_clicked(self, *args, **kwargs):
"""T & C 'back' button was clicked, return to the previous page."""
@@ -986,14 +967,18 @@
def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
"""The T&C page is being loaded."""
- if browser.get_load_status() == WEBKIT_LOAD_FINISHED:
+ from gi.repository import WebKit # pylint: disable=E0611
+
+ if browser.get_load_status().real == WebKit.LoadStatus.FINISHED:
self._set_current_page(self.tc_browser_vbox)
def on_tc_browser_navigation_requested(self, browser, frame, request,
action, decision, *args, **kwargs):
"""The user wants to navigate within the T&C browser."""
+ from gi.repository import WebKit # pylint: disable=E0611
+
if action is not None and \
- action.get_reason() == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED:
+ action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED:
if decision is not None:
decision.ignore()
url = action.get_original_uri()
=== modified file 'ubuntu_sso/gtk/tests/test_gui.py'
--- ubuntu_sso/gtk/tests/test_gui.py 2012-02-17 18:43:17 +0000
+++ ubuntu_sso/gtk/tests/test_gui.py 2012-03-20 16:10:09 +0000
@@ -134,7 +134,7 @@
def get_load_status(self):
"""Return the current load status."""
- return gui.WEBKIT_LOAD_FINISHED
+ return WebKit.LoadStatus.FINISHED
def show(self):
"""Show this instance."""
@@ -981,7 +981,7 @@
def test_notify_load_finished_connected(self):
"""The 'load-finished' signal is connected."""
expected = [self.ui.on_tc_browser_notify_load_status]
- self.assertEqual(self.browser._signals['load-finished'],
+ self.assertEqual(self.browser._signals['notify::load-status'],
expected)
def test_tc_loaded_morphs_into_tc_browser_vbox(self):
@@ -998,7 +998,7 @@
def test_navigation_requested_succeeds_for_no_clicking(self):
"""The navigation request succeeds when user hasn't clicked a link."""
action = WebKit.WebNavigationAction()
- action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_OTHER)
+ action.set_reason(WebKit.WebNavigationReason.OTHER)
decision = WebKit.WebPolicyDecision()
decision.use = self._set_called
@@ -1011,7 +1011,7 @@
def test_navigation_requested_ignores_clicked_links(self):
"""The navigation request is ignored if a link was clicked."""
action = WebKit.WebNavigationAction()
- action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
+ action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED)
decision = WebKit.WebPolicyDecision()
decision.ignore = self._set_called
@@ -1037,7 +1037,7 @@
"""
url = 'http://something.com/yadda'
action = WebKit.WebNavigationAction()
- action.set_reason(gui.WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
+ action.set_reason(WebKit.WebNavigationReason.LINK_CLICKED)
action.set_original_uri(url)
decision = WebKit.WebPolicyDecision()
@@ -1050,6 +1050,35 @@
self.ui.on_tc_browser_navigation_requested(**kwargs)
self.assertEqual(self._called, ((url,), {}))
+ def test_on_tc_button_clicked_no_child(self):
+ """Test the tc loading with no child."""
+ called = []
+
+ def fake_add_browser():
+ """Fake add browser."""
+ called.append('fake_add_browser')
+
+ self.patch(self.ui, '_add_webkit_browser', fake_add_browser)
+ self.patch(self.ui.tc_browser_window, 'get_child', lambda: None)
+
+ self.ui.on_tc_button_clicked()
+ self.assertIn('fake_add_browser', called)
+
+ def test_on_tc_button_clicked_child(self):
+ """Test the tc loading with child."""
+ called = []
+
+ def fake_add_browser(i_self):
+ """Fake add browser."""
+ called.append('fake_add_browser')
+
+ self.patch(self.ui, '_add_webkit_browser', fake_add_browser)
+
+ browser = WebKit.WebView()
+ self.ui.tc_browser_window.add(browser)
+ self.ui.on_tc_button_clicked()
+ self.assertNotIn('fake_add_browser', called)
+
class RegistrationErrorTestCase(UbuntuSSOClientTestCase):
"""Test suite for the user registration error handling."""
=== modified file 'ubuntu_sso/qt/__init__.py'
--- ubuntu_sso/qt/__init__.py 2012-03-05 18:56:50 +0000
+++ ubuntu_sso/qt/__init__.py 2012-03-20 16:10:09 +0000
@@ -18,19 +18,27 @@
import collections
-
-LINK_STYLE = (''
+from PyQt4 import QtGui, QtCore
+
+from ubuntu_sso.logger import setup_gui_logging
+from ubuntu_sso.utils.ui import GENERIC_BACKEND_ERROR
+
+logger = setup_gui_logging('ubuntu_sso.qt')
+
+LINK_STYLE = (u''
'{link_text}')
ERROR_ALL = '__all__'
-ERROR_STYLE = u'%s'
+ERROR_STYLE = u'%s'
ERROR_MESSAGE = 'message'
PREFERED_UI_SIZE = {'width': 550, 'height': 525}
-TITLE_STYLE = u'%s'
+TITLE_STYLE = u'%s'
+WINDOW_TITLE = 'Ubuntu Single Sign On'
# Based on the gtk implementation
def build_general_error_message(errordict):
"""Build a user-friendly error message from the errordict."""
+ logger.debug('build_general_error_message: errordict is: %r.', errordict)
result = ''
if isinstance(errordict, collections.Mapping):
msg1 = errordict.get(ERROR_ALL)
@@ -50,5 +58,25 @@
result = '\n'.join(
[('%s: %s' % (k, v)) for k, v in errordict.iteritems()])
else:
- result = repr(errordict)
+ result = GENERIC_BACKEND_ERROR
+ logger.error('build_general_error_message with unknown error: %r',
+ errordict)
+
+ logger.info('build_general_error_message: returning %r.', result)
return result
+
+
+def maybe_elide_text(label, text, width, markup=None):
+ """Set 'text' to be the 'label's text.
+
+ If 'text' is longer than 'width', set the label's tooltip to be the full
+ text, and the text itself to be the elided version of 'text'.
+
+ """
+ fm = QtGui.QFontMetrics(label.font())
+ elided_text = fm.elidedText(text, QtCore.Qt.ElideRight, width)
+ if elided_text != text:
+ label.setToolTip(text)
+ if markup is not None:
+ elided_text = markup % elided_text
+ label.setText(elided_text)
=== modified file 'ubuntu_sso/qt/common.py'
--- ubuntu_sso/qt/common.py 2012-02-16 14:13:36 +0000
+++ ubuntu_sso/qt/common.py 2012-03-20 16:10:09 +0000
@@ -28,9 +28,9 @@
)
# all the text + styles that are used in the gui
-BAD = u' %s '
-GOOD = u' %s '
-NORMAL = u' %s '
+BAD = u' %s '
+GOOD = u' %s '
+NORMAL = u' %s '
def password_assistance(line_edit, assistance, icon_type=BAD):
=== modified file 'ubuntu_sso/qt/current_user_sign_in_page.py'
--- ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/current_user_sign_in_page.py 2012-03-20 16:10:09 +0000
@@ -61,6 +61,11 @@
}
return result
+ @property
+ def password(self):
+ """Return the content of the password edit."""
+ return unicode(self.ui.password_edit.text())
+
def on_user_not_validated(self, app_name, email):
"""Show the validate email page."""
self.hide_overlay()
@@ -76,6 +81,7 @@
def initializePage(self):
"""Setup UI details."""
+ logger.debug('initializePage - About to show CurrentUserSignInPage')
self.setButtonText(QtGui.QWizard.CancelButton, CANCEL_BUTTON)
# Layout without custom button 1,
# without finish button
@@ -94,10 +100,8 @@
def _set_translated_strings(self):
"""Set the translated strings."""
- logger.debug('CurrentUserSignInPage._set_translated_strings')
self.setTitle(LOGIN_TITLE.format(app_name=self.app_name))
self.setSubTitle(LOGIN_SUBTITLE % {'app_name': self.app_name})
-
self.ui.email_label.setText(EMAIL_LABEL)
self.ui.password_label.setText(LOGIN_PASSWORD_LABEL)
forgotten_text = LINK_STYLE.format(link_url='#',
@@ -107,7 +111,6 @@
def _connect_ui(self):
"""Connect the buttons to perform actions."""
- logger.debug('CurrentUserSignInPage._connect_buttons')
self.ui.forgot_password_label.linkActivated.connect(
self.on_forgotten_password)
self.ui.email_edit.textChanged.connect(self._validate)
@@ -116,18 +119,16 @@
def _validate(self):
"""Perform input validation."""
- valid = True
correct_mail = is_correct_email(unicode(self.ui.email_edit.text()))
- password = unicode(self.ui.password_edit.text())
- if not correct_mail or not password:
- valid = False
- self.ui.sign_in_button.setEnabled(valid)
+ correct_password = len(unicode(self.ui.password_edit.text())) > 0
+ enabled = correct_mail and correct_password
+ self.ui.sign_in_button.setEnabled(enabled)
def login(self):
"""Perform the login using the self.backend."""
- logger.debug('CurrentUserSignInPage.login')
# grab the data from the view and call the backend
email = unicode(self.ui.email_edit.text())
+ logger.info('CurrentUserSignInPage.login for: %s', email)
password = unicode(self.ui.password_edit.text())
args = (self.app_name, email, password)
if self.ping_url:
@@ -146,18 +147,18 @@
# let the user know
logger.error('Got error when login %s, error: %s',
self.app_name, error)
- self.show_error(self.app_name, build_general_error_message(error))
+ self.show_error(build_general_error_message(error))
def on_logged_in(self, app_name, result):
"""We managed to log in."""
logger.info('Logged in for %s', app_name)
self.hide_overlay()
email = unicode(self.ui.email_edit.text())
+ logger.debug('About to emit userLoggedIn signal with: (%s).', email)
self.userLoggedIn.emit(email)
- logger.debug('Wizard.loginSuccess emitted.')
def on_forgotten_password(self, link=None):
"""Show the user the forgotten password page."""
- logger.info('Forgotten password')
self.hide_overlay()
+ logger.debug('About to emit passwordForgotten signal')
self.passwordForgotten.emit()
=== modified file 'ubuntu_sso/qt/email_verification_page.py'
--- ubuntu_sso/qt/email_verification_page.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/email_verification_page.py 2012-03-20 16:10:09 +0000
@@ -71,7 +71,6 @@
def _connect_ui(self):
"""Set the connection of signals."""
- logger.debug('EmailVerificationController._connect_ui')
self.ui.verification_code_edit.textChanged.connect(
self.validate_form)
self.next_button.clicked.connect(self.validate_email)
@@ -84,7 +83,6 @@
def _set_translated_strings(self):
"""Set the different titles."""
- logger.debug('EmailVerificationController._set_titles')
self.header.set_title(VERIFY_EMAIL_TITLE)
self.header.set_subtitle(VERIFY_EMAIL_CONTENT % {
"app_name": self.app_name,
@@ -103,7 +101,8 @@
def validate_email(self):
"""Call the next action."""
- logger.debug('EmailVerificationController.validate_email')
+ logger.debug('EmailVerificationController.validate_email for: %s',
+ self.email)
code = unicode(self.ui.verification_code_edit.text())
args = (self.app_name, self.email, self.password, code)
self.hide_error()
@@ -123,21 +122,25 @@
def on_email_validated(self, app_name, email):
"""Signal thrown after the email is validated."""
- logger.info('EmailVerificationController.on_email_validated')
+ logger.info('EmailVerificationController.on_email_validated for %s, '
+ 'email: %s', app_name, email)
self.hide_overlay()
self.registrationSuccess.emit(self.email)
def on_email_validation_error(self, app_name, error):
"""Signal thrown when there's a problem validating the email."""
+ logger.error('Got error on email validation %s, error: %s',
+ app_name, error)
self.hide_overlay()
msg = error.pop(ERROR_EMAIL_TOKEN, '')
msg += build_general_error_message(error)
- self.show_error(self.app_name, msg)
+ self.show_error(msg)
# pylint: disable=C0103
def initializePage(self):
"""Called to prepare the page just before it is shown."""
+ logger.debug('initializePage - About to show EmailVerificationPage')
self.next_button.setDefault(True)
self.next_button.setEnabled(False)
self.wizard().setButtonLayout([QtGui.QWizard.Stretch])
=== modified file 'ubuntu_sso/qt/enhanced_check_box.py'
--- ubuntu_sso/qt/enhanced_check_box.py 2012-03-01 16:53:29 +0000
+++ ubuntu_sso/qt/enhanced_check_box.py 2012-03-20 16:10:09 +0000
@@ -24,11 +24,12 @@
class EnhancedCheckBox(QtGui.QCheckBox):
"""Enhanced QCheckBox to support links in the message displayed."""
- def __init__(self, text=""):
- QtGui.QCheckBox.__init__(self)
+ def __init__(self, text="", parent=None):
+ QtGui.QCheckBox.__init__(self, parent)
hbox = QtGui.QHBoxLayout()
+ hbox.setAlignment(QtCore.Qt.AlignLeft)
self.text_label = QtGui.QLabel(text)
- self.text_label.setAlignment(QtCore.Qt.AlignTop)
+ self.text_label.setWordWrap(True)
self.text_label.setOpenExternalLinks(True)
padding = self.iconSize().width()
self.text_label.setStyleSheet("margin-top: -3px;"
@@ -37,6 +38,11 @@
hbox.addWidget(self.text_label)
self.setLayout(hbox)
+ if parent is not None:
+ lines = self.text_label.width() / float(parent.width())
+ self.text_label.setMinimumWidth(parent.width())
+ self.setMinimumHeight(self.height() * lines)
+
self.stateChanged.connect(self.text_label.setFocus)
def text(self):
=== modified file 'ubuntu_sso/qt/forgotten_password_page.py'
--- ubuntu_sso/qt/forgotten_password_page.py 2012-03-05 20:31:22 +0000
+++ ubuntu_sso/qt/forgotten_password_page.py 2012-03-20 16:10:09 +0000
@@ -63,6 +63,7 @@
def initializePage(self):
"""Set the initial state of ForgottenPassword page."""
+ logger.debug('initializePage - About to show ForgottenPasswordPage')
self.ui.send_button.setDefault(True)
enabled = not self.ui.email_line_edit.text().isEmpty()
self.ui.send_button.setEnabled(enabled)
@@ -98,6 +99,7 @@
"""Send the request password operation."""
self.hide_error()
args = (self.app_name, self.email_address)
+ logger.debug('Sending request new password for %s, email: %s', *args)
f = self.backend.request_password_reset_token
error_handler = partial(self._handle_error, f,
@@ -113,6 +115,8 @@
def on_password_reset_token_sent(self, app_name, email):
"""Action taken when we managed to get the password reset done."""
+ logger.info('ForgottenPasswordPage.on_password_reset_token_sent for '
+ '%s, email: %s', app_name, email)
# ignore the result and move to the reset page
self.hide_overlay()
self.passwordResetTokenSent.emit(email)
@@ -123,4 +127,4 @@
# set the error message
self.hide_overlay()
msg = REQUEST_PASSWORD_TOKEN_WRONG_EMAIL
- self.show_error(self.app_name, msg)
+ self.show_error(msg)
=== modified file 'ubuntu_sso/qt/loadingoverlay.py'
--- ubuntu_sso/qt/loadingoverlay.py 2012-02-27 19:00:59 +0000
+++ ubuntu_sso/qt/loadingoverlay.py 2012-03-20 16:10:09 +0000
@@ -21,6 +21,8 @@
from ubuntu_sso.qt.ui import loadingoverlay_ui
from ubuntu_sso.utils.ui import LOADING_OVERLAY
+LOADING_STYLE = u'{0}'
+
class LoadingOverlay(QtGui.QFrame):
"""The widget that shows a loading animation and disable the widget below.
@@ -43,7 +45,7 @@
self.counter = 0
self.orientation = False
- self.ui.label.setText(LOADING_OVERLAY)
+ self.ui.label.setText(LOADING_STYLE.format(LOADING_OVERLAY))
# Invalid name "paintEvent", "eventFilter", "showEvent", "timerEvent"
# pylint: disable=C0103
=== modified file 'ubuntu_sso/qt/main.py'
--- ubuntu_sso/qt/main.py 2012-02-24 19:54:48 +0000
+++ ubuntu_sso/qt/main.py 2012-03-20 16:10:09 +0000
@@ -25,6 +25,7 @@
from ubuntu_sso.qt.ui import resources_rc
# pylint: enable=W0611
from ubuntu_sso.qt.ubuntu_sso_wizard import UbuntuSSOClientGUI
+from ubuntu_sso.utils import PLATFORM_QSS
def main(**kwargs):
@@ -34,9 +35,17 @@
QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf')
QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf')
- # Apply Style Sheet -- The windows version may be different
- qss = QtCore.QResource(":/stylesheet.qss")
- app.setStyleSheet(qss.data())
+ data = []
+ for qss_name in (PLATFORM_QSS, ":/stylesheet.qss"):
+ qss = QtCore.QResource(qss_name)
+ data.append(unicode(qss.data()))
+ app.setStyleSheet('\n'.join(data))
+
+ # Fix the string that contains unicode chars.
+ for key in kwargs:
+ value = kwargs[key]
+ if isinstance(value, str):
+ kwargs[key] = value.decode('utf-8')
# Unused variable 'ui', pylint: disable=W0612
ui = UbuntuSSOClientGUI(close_callback=app.exit, **kwargs)
=== modified file 'ubuntu_sso/qt/network_detection_page.py'
--- ubuntu_sso/qt/network_detection_page.py 2012-02-29 14:00:12 +0000
+++ ubuntu_sso/qt/network_detection_page.py 2012-03-20 16:10:09 +0000
@@ -20,7 +20,7 @@
from PyQt4 import QtGui
from ubuntu_sso import networkstate
-
+from ubuntu_sso.logger import setup_logging
from ubuntu_sso.qt.sso_wizard_page import SSOWizardPage
from ubuntu_sso.qt.ui import network_detection_ui
from ubuntu_sso.utils.ui import (
@@ -31,6 +31,9 @@
)
+logger = setup_logging('ubuntu_sso.network_detection_page')
+
+
class NetworkDetectionPage(SSOWizardPage):
"""Widget to show if we don't detect a network connection."""
@@ -48,6 +51,7 @@
def initializePage(self):
"""Set UI details."""
+ logger.debug('initializePage - About to show NetworkDetectionPage')
self.wizard()._next_id = None
self.setButtonText(QtGui.QWizard.CustomButton1, TRY_AGAIN_BUTTON)
=== modified file 'ubuntu_sso/qt/proxy_dialog.py'
--- ubuntu_sso/qt/proxy_dialog.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/proxy_dialog.py 2012-03-20 16:10:09 +0000
@@ -21,6 +21,7 @@
from PyQt4.QtGui import QApplication, QDialog, QIcon
from twisted.internet import defer
+from ubuntu_sso import EXCEPTION_RAISED, USER_SUCCESS, USER_CANCELLATION
from ubuntu_sso.logger import setup_gui_logging
from ubuntu_sso.keyring import Keyring
from ubuntu_sso.qt.ui.proxy_credentials_dialog_ui import Ui_ProxyCredsDialog
@@ -37,10 +38,6 @@
PROXY_CREDS_SAVE_BUTTON,
)
-CREDS_ACQUIRED = 0
-USER_CANCELATION = -1
-EXCEPTION_RAISED = -2
-
logger = setup_gui_logging("ubuntu_sso.qt.proxy_dialog")
@@ -107,15 +104,15 @@
logger.debug('Save credentials as for domain %s.', self.domain)
yield self.keyring.set_credentials(self.domain, creds)
except Exception, e:
- logger.error('Could not retrieve credentials.')
+ logger.exception('Could not set credentials:')
self.done(EXCEPTION_RAISED)
# pylint: disable=W0703, W0612
- self.done(CREDS_ACQUIRED)
+ self.done(USER_SUCCESS)
def _on_cancel_clicked(self, *args):
"""End the dialog."""
logger.debug('User canceled credentials dialog.')
- self.done(USER_CANCELATION)
+ self.done(USER_CANCELLATION)
def _set_buttons(self):
"""Set the labels of the buttons."""
=== modified file 'ubuntu_sso/qt/reset_password_page.py'
--- ubuntu_sso/qt/reset_password_page.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/reset_password_page.py 2012-03-20 16:10:09 +0000
@@ -18,7 +18,7 @@
from functools import partial
-from PyQt4.QtCore import SIGNAL, pyqtSignal
+from PyQt4.QtCore import Qt, SIGNAL, pyqtSignal
from PyQt4.QtGui import QApplication
from ubuntu_sso import NO_OP
@@ -73,7 +73,9 @@
def initializePage(self):
"""Extends QWizardPage initializePage method."""
+ logger.debug('initializePage - About to show ResetPasswordPage')
super(ResetPasswordPage, self).initializePage()
+ self.ui.gridLayout.setAlignment(Qt.AlignLeft)
common.password_default_assistance(self.ui.password_assistance)
self.ui.password_assistance.setVisible(False)
self.setTitle(RESET_TITLE)
@@ -158,6 +160,8 @@
def on_password_changed(self, app_name, email):
"""Let user know that the password was changed."""
+ logger.info('ResetPasswordPage.on_password_changed for %s, email: %s',
+ app_name, email)
self.hide_overlay()
email = unicode(self.wizard().forgotten.ui.email_line_edit.text())
self.passwordChanged.emit(email)
@@ -166,7 +170,7 @@
"""Let the user know that there was an error."""
logger.error('Got error changing password for %s, error: %s',
self.app_name, error)
- self.show_error(self.app_name, build_general_error_message(error))
+ self.show_error(build_general_error_message(error))
def set_new_password(self):
"""Request a new password to be set."""
=== modified file 'ubuntu_sso/qt/setup_account_page.py'
--- ubuntu_sso/qt/setup_account_page.py 2012-03-06 14:13:10 +0000
+++ ubuntu_sso/qt/setup_account_page.py 2012-03-20 16:10:09 +0000
@@ -31,7 +31,7 @@
from PyQt4 import QtGui, QtCore
from ubuntu_sso import NO_OP
-from ubuntu_sso.logger import setup_gui_logging
+from ubuntu_sso.logger import setup_gui_logging, log_call
from ubuntu_sso.qt import (
LINK_STYLE,
build_general_error_message,
@@ -112,11 +112,17 @@
}
return result
+ @property
+ def password(self):
+ """Return the content of the password edit."""
+ return unicode(self.ui.password_edit.text())
+
# Invalid name "initializePage"
# pylint: disable=C0103
def initializePage(self):
"""Setup UI details."""
+ logger.debug('initializePage - About to show SetupAccountPage')
# Set Setup Account button
self.wizard().setOption(QtGui.QWizard.HaveCustomButton3, True)
try:
@@ -152,7 +158,6 @@
def _set_translated_strings(self):
"""Set the strings."""
- logger.debug('SetUpAccountPage._set_translated_strings')
# set the translated string
title_page = REGISTER_TITLE.format(app_name=self.app_name)
self.setTitle(title_page)
@@ -190,7 +195,7 @@
terms = AGREE_TO_PRIVACY_POLICY.format(app_name=self.app_name,
privacy_policy=privacy_policy_link)
- self.terms_checkbox = enhanced_check_box.EnhancedCheckBox(terms)
+ self.terms_checkbox = enhanced_check_box.EnhancedCheckBox(terms, self)
self.ui.hlayout_check.addWidget(self.terms_checkbox)
self.terms_checkbox.setVisible(bool(self.tc_url or self.policy_url))
@@ -220,7 +225,6 @@
def _connect_ui(self):
"""Set the connection of signals."""
- logger.debug('SetUpAccountPage._connect_ui')
self._set_line_edits_validations()
self.ui.captcha_view.setPixmap(QtGui.QPixmap())
@@ -229,6 +233,7 @@
self.ui.password_assistance,
common.NORMAL))
+ self.ui.refresh_label.linkActivated.connect(self.hide_error)
self.ui.refresh_label.linkActivated.connect(lambda url: \
self._refresh_captcha())
# We need to check if we enable the button on many signals
@@ -271,7 +276,6 @@
def _refresh_captcha(self):
"""Refresh the captcha image shown in the ui."""
logger.debug('SetUpAccountPage._refresh_captcha')
- self.hide_error()
# lets clean behind us, do we have the old file arround?
if self.captcha_file and os.path.exists(self.captcha_file):
os.unlink(self.captcha_file)
@@ -297,11 +301,9 @@
self.registerField('email_address', self.ui.email_edit)
self.registerField('password', self.ui.password_edit)
+ @log_call(logger.debug)
def on_captcha_generated(self, app_name, result):
"""A new image was generated."""
- logger.debug('SetUpAccountPage.on_captcha_generated for %r '
- '(captcha id %r, filename %r).',
- app_name, result, self.captcha_file)
self.captcha_id = result
# HACK: First, let me apologize before hand, you can mention my mother
# if needed I would do the same (mandel)
@@ -320,28 +322,28 @@
self.captcha_image = pixmap_image
self.on_captcha_refresh_complete()
- def on_captcha_generation_error(self, error, *args, **kwargs):
+ @log_call(logger.error)
+ def on_captcha_generation_error(self, app_name, error):
"""An error ocurred."""
- logger.debug('SetUpAccountPage.on_captcha_generation_error')
- self.show_error(self.app_name, CAPTCHA_LOAD_ERROR)
+ self.show_error(CAPTCHA_LOAD_ERROR)
self.on_captcha_refresh_complete()
+ @log_call(logger.error)
def on_user_registration_error(self, app_name, error):
"""Let the user know we could not register."""
- logger.debug('SetUpAccountPage.on_user_registration_error')
# errors are returned as a dict with the data we want to show.
msg = error.pop(ERROR_EMAIL, '')
if msg:
self.set_error_message(self.ui.email_assistance, msg)
error_msg = build_general_error_message(error)
if error_msg:
- self.show_error(self.app_name, error_msg)
+ self.show_error(error_msg)
self._refresh_captcha()
+ @log_call(logger.info)
def on_user_registered(self, app_name, email):
"""Execute when the user did register."""
self.hide_overlay()
- logger.debug('SetUpAccountPage.on_user_registered')
email = unicode(self.ui.email_edit.text())
self.userRegistered.emit(email)
@@ -376,7 +378,7 @@
messages.append(CAPTCHA_REQUIRED_ERROR)
if len(messages) > 0:
condition = False
- self.show_error(self.app_name, '\n'.join(messages))
+ self.show_error('\n'.join(messages))
return condition
def set_next_validation(self):
@@ -401,17 +403,14 @@
def is_correct_email(self, email_address):
"""Return if the email is correct."""
- logger.debug('SetUpAccountPage.is_correct_email')
return '@' in email_address
def is_correct_email_confirmation(self, email_address):
"""Return that the email is the same."""
- logger.debug('SetUpAccountPage.is_correct_email_confirmation')
return unicode(self.ui.email_edit.text()) == email_address
def is_correct_password_confirmation(self, password):
"""Return that the passwords are correct."""
- logger.debug('SetUpAccountPage.is_correct_password_confirmation')
return unicode(self.ui.password_edit.text()) == password
def focus_changed(self, old, now):
@@ -500,12 +499,14 @@
def on_captcha_refreshing(self):
"""Show overlay when captcha is refreshing."""
+ logger.info('SetUpAccountPage.on_captcha_refreshing')
if self.isVisible():
self.show_overlay()
self.captcha_received = False
def on_captcha_refresh_complete(self):
"""Hide overlay when captcha finished refreshing."""
+ logger.info('SetUpAccountPage.on_captcha_refresh_complete')
self.hide_overlay()
self.captcha_received = True
=== modified file 'ubuntu_sso/qt/sso_wizard_page.py'
--- ubuntu_sso/qt/sso_wizard_page.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/sso_wizard_page.py 2012-03-20 16:10:09 +0000
@@ -23,31 +23,38 @@
QApplication,
QCursor,
QFrame,
- QFontMetrics,
QHBoxLayout,
+ QLabel,
+ QStyle,
QVBoxLayout,
- QStyle,
QWizardPage,
- QLabel,
)
from twisted.internet import defer
from ubuntu_sso import main
-from ubuntu_sso.logger import setup_gui_logging
-from ubuntu_sso.qt import PREFERED_UI_SIZE, TITLE_STYLE
+from ubuntu_sso.logger import setup_gui_logging, log_call
+from ubuntu_sso.qt import (
+ ERROR_STYLE,
+ maybe_elide_text,
+ PREFERED_UI_SIZE,
+ TITLE_STYLE,
+)
from ubuntu_sso.utils.ui import GENERIC_BACKEND_ERROR
logger = setup_gui_logging('ubuntu_sso.sso_wizard_page')
-class Header(QFrame):
- """Header Class for Title and Subtitle in all wizard pages."""
+class WizardHeader(QFrame):
+ """WizardHeader Class for Title and Subtitle in all wizard pages."""
- def __init__(self):
+ def __init__(self, max_width, parent=None):
"""Create a new instance."""
- super(Header, self).__init__()
- self.setObjectName('header')
+ super(WizardHeader, self).__init__(parent=parent)
+ self.max_width = max_width
+ self.max_title_width = self.max_width * 0.95
+ self.max_subtitle_width = self.max_width * 1.8
+
vbox = QVBoxLayout(self)
vbox.setContentsMargins(0, 0, 0, 0)
self.title_label = QLabel()
@@ -64,11 +71,8 @@
def set_title(self, title):
"""Set the Title of the page or hide it otherwise"""
if title:
- fm = QFontMetrics(self.subtitle_label.font())
- width = PREFERED_UI_SIZE['width'] * 0.95
- elided_text = fm.elidedText(title, Qt.ElideRight, width)
- self.title_label.setToolTip(title)
- self.title_label.setText(elided_text)
+ maybe_elide_text(self.title_label, title, self.max_title_width,
+ markup=TITLE_STYLE)
self.title_label.setVisible(True)
else:
self.title_label.setVisible(False)
@@ -76,28 +80,23 @@
def set_subtitle(self, subtitle):
"""Set the Subtitle of the page or hide it otherwise"""
if subtitle:
- fm = QFontMetrics(self.subtitle_label.font())
- width = PREFERED_UI_SIZE['width'] * 1.8
- elided_text = fm.elidedText(subtitle, Qt.ElideRight, width)
- self.subtitle_label.setText(elided_text)
- self.subtitle_label.setToolTip(subtitle)
+ maybe_elide_text(self.subtitle_label, subtitle,
+ self.max_subtitle_width)
self.subtitle_label.setVisible(True)
else:
self.subtitle_label.setVisible(False)
-class SSOWizardPage(QWizardPage):
- """Root class for all wizard pages."""
+class BaseWizardPage(QWizardPage):
+ """Base class for all wizard pages."""
ui_class = None
- _signals = {} # override in children
+ max_width = 0
processingStarted = pyqtSignal()
processingFinished = pyqtSignal()
- def __init__(self, app_name, **kwargs):
- """Create a new instance."""
- parent = kwargs.pop('parent', None)
- super(SSOWizardPage, self).__init__(parent=parent)
+ def __init__(self, parent=None):
+ super(BaseWizardPage, self).__init__(parent=parent)
self.ui = None
if self.ui_class is not None:
@@ -105,64 +104,39 @@
self.ui = self.ui_class()
self.ui.setupUi(self)
- # store common useful data provided by the app
- self.app_name = app_name
- self.ping_url = kwargs.get('ping_url', '')
- self.tc_url = kwargs.get('tc_url', '')
- self.policy_url = kwargs.get('policy_url', '')
- self.help_text = kwargs.get('help_text', '')
+ if self.layout() is None:
+ self.setLayout(QVBoxLayout(self))
# Set the error area
- self.form_errors_label = QLabel(' ')
+ self.form_errors_label = QLabel()
self.form_errors_label.setObjectName('form_errors')
self.form_errors_label.setAlignment(Qt.AlignBottom)
self.layout().insertWidget(0, self.form_errors_label)
+
# Set the header
- self.header = Header()
+ self.header = WizardHeader(max_width=self.max_width)
self.header.set_title(title='')
self.header.set_subtitle(subtitle='')
self.layout().insertWidget(0, self.header)
- self._signals_receivers = {}
- self.backend = None
self.layout().setAlignment(Qt.AlignLeft)
- self.setup_page()
-
- def show_error(self, app_name, message):
- """Show an error message inside the page."""
- self.hide_overlay()
- fm = QFontMetrics(self.form_errors_label.font())
- width = PREFERED_UI_SIZE['width'] * 0.95
- elided_text = fm.elidedText(message, Qt.ElideRight, width)
- self.form_errors_label.setText(elided_text)
- self.form_errors_label.setToolTip(message)
-
- def hide_error(self):
- """Hide the label errors in the current page."""
- # We actually want the label with one chat, because if it is an
- # empty string, the height of the label is 0
- self.form_errors_label.setText(' ')
-
- def hide_overlay(self):
- """Emit the signal to notify the upper container that ends loading."""
- self.setEnabled(True)
- self.processingFinished.emit()
-
- def show_overlay(self):
- """Emit the signal to notify the upper container that is loading."""
- self.setEnabled(False)
- self.processingStarted.emit()
-
- @defer.inlineCallbacks
- def setup_page(self):
- """Setup the widget components."""
- client = yield main.get_sso_client()
- self.backend = client.sso_login
-
- self._setup_signals()
- self._set_translated_strings()
- self._connect_ui()
+ self._is_processing = False
+
+ def _get_is_processing(self):
+ """Is this widget processing any request?"""
+ return self._is_processing
+
+ def _set_is_processing(self, new_value):
+ """Set this widget to be processing a request."""
+ self._is_processing = new_value
+ self.setEnabled(not new_value)
+ if not self._is_processing:
+ self.processingFinished.emit()
+ else:
+ self.processingStarted.emit()
+
+ is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
# pylint: disable=C0103
@@ -172,7 +146,7 @@
def setTitle(self, title=''):
"""Set the Wizard Page Title."""
- self.header.set_title(TITLE_STYLE % title)
+ self.header.set_title(title)
def setSubTitle(self, subtitle=''):
"""Set the Wizard Page Subtitle."""
@@ -188,6 +162,74 @@
# pylint: enable=C0103
+ @log_call(logger.error)
+ def show_error(self, message):
+ """Show an error message inside the page."""
+ self.is_processing = False
+ maybe_elide_text(self.form_errors_label, message,
+ self.max_width * 0.95, markup=ERROR_STYLE)
+
+ def hide_error(self):
+ """Hide the label errors in the current page."""
+ # We actually want the label with one empty char, because if it is an
+ # empty string, the height of the label is 0
+ self.form_errors_label.setText(' ')
+
+
+class SSOWizardPage(BaseWizardPage):
+ """Root class for all SSO specific wizard pages."""
+
+ _signals = {} # override in children
+ max_width = PREFERED_UI_SIZE['width']
+
+ def __init__(self, app_name, **kwargs):
+ """Create a new instance."""
+ parent = kwargs.pop('parent', None)
+ super(SSOWizardPage, self).__init__(parent=parent)
+
+ # store common useful data provided by the app
+ self.app_name = app_name
+ self.ping_url = kwargs.get('ping_url', '')
+ self.tc_url = kwargs.get('tc_url', '')
+ self.policy_url = kwargs.get('policy_url', '')
+ self.help_text = kwargs.get('help_text', '')
+
+ self._signals_receivers = {}
+ self.backend = None
+
+ self.setup_page()
+
+ def hide_overlay(self):
+ """Emit the signal to notify the upper container that ends loading."""
+ self.is_processing = False
+
+ def show_overlay(self):
+ """Emit the signal to notify the upper container that is loading."""
+ self.is_processing = True
+
+ @defer.inlineCallbacks
+ def setup_page(self):
+ """Setup the widget components."""
+ logger.info('Starting setup_page for: %r', self)
+ # pylint: disable=W0702,W0703
+ try:
+ # Get Backend
+ client = yield main.get_sso_client()
+ self.backend = client.sso_login
+ self._set_translated_strings()
+ self._connect_ui()
+ # Call _setup_signals at the end, so we ensure that the UI
+ # is at least styled as expected if the operations with the
+ # backend fails.
+ self._setup_signals()
+ except:
+ message = 'There was a problem trying to setup the page %r' % self
+ self.show_error(message)
+ logger.exception(message)
+ self.setEnabled(False)
+ # pylint: enable=W0702,W0703
+ logger.info('%r - setup_page ends, backend is %r.', self, self.backend)
+
def _filter_by_app_name(self, f):
"""Excecute the decorated function only for 'self.app_name'."""
@@ -218,11 +260,9 @@
def _set_translated_strings(self):
"""Implement in each child."""
- raise NotImplementedError()
def _connect_ui(self):
"""Implement in each child."""
- raise NotImplementedError()
def _handle_error(self, remote_call, handler, error):
"""Handle any error when calling the remote backend."""
=== modified file 'ubuntu_sso/qt/tests/__init__.py'
--- ubuntu_sso/qt/tests/__init__.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/tests/__init__.py 2012-03-20 16:10:09 +0000
@@ -21,7 +21,7 @@
from twisted.trial.unittest import TestCase
from ubuntu_sso import main, NO_OP
-from ubuntu_sso.qt import TITLE_STYLE
+from ubuntu_sso.qt import ERROR_STYLE, maybe_elide_text, TITLE_STYLE
from ubuntu_sso.tests import (
APP_NAME,
HELP_TEXT,
@@ -38,6 +38,15 @@
# pylint: disable=W0212
+def build_string_for_pixels(label, width):
+ """Return a random string that will be as big as with in pixels."""
+ char = 'a'
+ fm = QtGui.QFontMetrics(label.font())
+ pixel_width = fm.width(char)
+ chars = int(width / pixel_width)
+ return char * chars
+
+
class FakedObject(object):
"""Fake an object, record every call."""
@@ -439,6 +448,9 @@
self.wizard = self.ui_wizard_class()
self.patch(self.ui, 'wizard', lambda: self.wizard)
+ self.ui.show()
+ self.addCleanup(self.ui.hide)
+
def _set_called(self, *args, **kwargs):
"""Store 'args' and 'kwargs' for test assertions."""
self._called = (args, kwargs)
@@ -467,6 +479,38 @@
self.assertEqual(self.signal_results, [signal_args])
+ def assert_title_correct(self, title_label, expected, max_width):
+ """Check that the label's text is equal to 'expected'."""
+ label = QtGui.QLabel()
+ maybe_elide_text(label, expected, max_width)
+
+ self.assertEqual(TITLE_STYLE % unicode(label.text()),
+ unicode(title_label.text()))
+ self.assertEqual(unicode(label.toolTip()),
+ unicode(title_label.toolTip()))
+ self.assertTrue(title_label.isVisible())
+
+ def assert_subtitle_correct(self, subtitle_label, expected, max_width):
+ """Check that the subtitle is equal to 'expected'."""
+ label = QtGui.QLabel()
+ maybe_elide_text(label, expected, max_width)
+
+ self.assertEqual(unicode(label.text()), unicode(subtitle_label.text()))
+ self.assertEqual(unicode(label.toolTip()),
+ unicode(subtitle_label.toolTip()))
+ self.assertTrue(subtitle_label.isVisible())
+
+ def assert_error_correct(self, error_label, expected, max_width):
+ """Check that the error 'error_label' displays 'expected' as text."""
+ label = QtGui.QLabel()
+ maybe_elide_text(label, expected, max_width)
+
+ self.assertEqual(ERROR_STYLE % unicode(label.text()),
+ unicode(error_label.text()))
+ self.assertEqual(unicode(label.toolTip()),
+ unicode(error_label.toolTip()))
+ self.assertTrue(error_label.isVisible())
+
def get_pixmap_data(self, pixmap):
"""Get the raw data of a QPixmap."""
byte_array = QtCore.QByteArray()
@@ -507,7 +551,7 @@
self.assertIn(signal, self.ui._signals)
self.assertTrue(callable(self.ui._signals[signal]))
- expected = ['_setup_signals', '_set_translated_strings', '_connect_ui']
+ expected = ['_set_translated_strings', '_connect_ui', '_setup_signals']
self.assertEqual(expected, called)
@@ -550,14 +594,18 @@
self.assertEqual(self._overlay_hide_counter, 1)
self.assertTrue(self.ui.isEnabled())
+ # pylint: disable=W0221
+
def assert_title_correct(self, expected):
"""Check that the title is equal to 'expected'."""
- self.assertEqual(TITLE_STYLE % expected, unicode(self.ui.title()))
+ check = super(PageBaseTestCase, self).assert_title_correct
+ check(self.ui.header.title_label, expected,
+ self.ui.header.max_title_width)
def assert_subtitle_correct(self, expected):
"""Check that the subtitle is equal to 'expected'."""
- elided_text = unicode(self.ui.subTitle())
- elided_text = elided_text[:len(elided_text) - 1]
+ check = super(PageBaseTestCase, self).assert_subtitle_correct
+ check(self.ui.header.subtitle_label, expected,
+ self.ui.header.max_subtitle_width)
- self.assertTrue(expected.startswith(elided_text))
- self.assertEqual(self.ui.header.subtitle_label.toolTip(), expected)
+ # pylint: enable=W0221
=== modified file 'ubuntu_sso/qt/tests/login_u_p.py'
--- ubuntu_sso/qt/tests/login_u_p.py 2012-01-26 15:34:16 +0000
+++ ubuntu_sso/qt/tests/login_u_p.py 2012-03-20 16:10:09 +0000
@@ -39,7 +39,6 @@
def found(*args):
"""The result was received."""
- print "result received", args
d.callback(args)
client.cred_manager.connect_to_signal('CredentialsFound', found)
@@ -61,7 +60,6 @@
yield cleared
yield client.cred_manager.login_email_password('SUPER', args)
- print "called ok"
yield d
yield client.disconnect()
=== modified file 'ubuntu_sso/qt/tests/show_gui.py'
--- ubuntu_sso/qt/tests/show_gui.py 2012-02-10 17:18:22 +0000
+++ ubuntu_sso/qt/tests/show_gui.py 2012-03-20 16:10:09 +0000
@@ -46,7 +46,6 @@
def found(*args):
"""The result was received."""
- print "result received", args
d.callback(args)
client.cred_manager.connect_to_signal('CredentialsFound', found)
@@ -62,7 +61,6 @@
TC_URL_KEY: u'http://www.google.com/',
UI_EXECUTABLE_KEY: 'ubuntu-sso-login-gtk',
})
- print "called ok"
yield d
yield client.disconnect()
=== modified file 'ubuntu_sso/qt/tests/test_common.py'
--- ubuntu_sso/qt/tests/test_common.py 2011-10-28 10:41:18 +0000
+++ ubuntu_sso/qt/tests/test_common.py 2012-03-20 16:10:09 +0000
@@ -20,7 +20,13 @@
from twisted.internet import defer
from twisted.trial.unittest import TestCase
-from ubuntu_sso.qt.common import (check_as_invalid,
+from ubuntu_sso.qt import (
+ build_general_error_message,
+ maybe_elide_text,
+ GENERIC_BACKEND_ERROR,
+)
+from ubuntu_sso.qt.common import (
+ check_as_invalid,
check_as_valid,
password_assistance,
password_check_match,
@@ -30,7 +36,9 @@
PASSWORD_DIGIT,
PASSWORD_LENGTH,
PASSWORD_MATCH,
- PASSWORD_UPPER)
+ PASSWORD_UPPER,
+)
+from ubuntu_sso.qt.tests import build_string_for_pixels
class PasswordTestCase(TestCase):
@@ -234,3 +242,123 @@
line_edit = QtGui.QLineEdit()
check_as_invalid(line_edit)
self.assertTrue(line_edit.property("formError").toBool())
+
+
+class ElidedTextTestCase(TestCase):
+ """The test case for the maybe_elide_text function."""
+
+ max_width = 100
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ """Setup tests."""
+ yield super(ElidedTextTestCase, self).setUp()
+ self.ui = QtGui.QLabel()
+
+ def test_text_not_elided_if_too_short(self):
+ """If text is shorter than max_width, do not elide."""
+ text = build_string_for_pixels(self.ui, self.max_width - 1)
+
+ maybe_elide_text(self.ui, text, self.max_width)
+
+ self.assertEqual(self.ui.toolTip(), '')
+ self.assertEqual(self.ui.text(), text)
+ self.assertNotIn(u'\u2026', self.ui.text())
+
+ def test_text_not_elided_if_equals_max_width(self):
+ """If text is equal than max_width, do not elide."""
+ text = build_string_for_pixels(self.ui, self.max_width)
+
+ maybe_elide_text(self.ui, text, self.max_width)
+
+ self.assertEqual(self.ui.toolTip(), '')
+ self.assertEqual(self.ui.text(), text)
+ self.assertNotIn(u'\u2026', self.ui.text())
+
+ def test_text_elided_if_bigger_than_max_width(self):
+ """If text is equal than max_width, do not elide."""
+ text = build_string_for_pixels(self.ui, self.max_width + 10)
+
+ maybe_elide_text(self.ui, text, self.max_width)
+
+ self.assertEqual(self.ui.toolTip(), text)
+ expected = unicode(self.ui.text())
+ self.assertTrue(expected.endswith(u'\u2026'))
+ self.assertTrue(text.startswith(expected[:-1]))
+
+
+class BuildGeneralErrorMessageTestCase(TestCase):
+ """Test passwords conditions."""
+
+ def test_with_message(self):
+ """Test build_general_error_message with 'message' key."""
+ error = "error message"
+ err_dict = {'message': error}
+
+ result = build_general_error_message(err_dict)
+
+ self.assertEqual(result, error)
+
+ def test_with_all(self):
+ """Test build_general_error_message with 'all' key."""
+ error = "error message"
+ err_dict = {'__all__': error}
+
+ result = build_general_error_message(err_dict)
+
+ self.assertEqual(result, error)
+
+ def test_with_message_and_all(self):
+ """Test build_general_error_message with 'all' and 'message' key."""
+ error = "error message"
+ error2 = "error message2"
+ err_dict = {'__all__': error, 'message': error2}
+
+ result = build_general_error_message(err_dict)
+
+ expected = '\n'.join((error, error2))
+ self.assertEqual(result, expected)
+
+ def test_with_all_and_error_message(self):
+ """Test for 'all' and 'error_message' key."""
+ error = "error message"
+ error2 = "error message2"
+ err_dict = {'__all__': error, 'error_message': error2}
+ result = build_general_error_message(err_dict)
+ expected = '\n'.join((error, error2))
+ self.assertEqual(result, expected)
+
+ def test_with_random_keys(self):
+ """Test build_general_error_message with random keys."""
+ error = "error message"
+ error2 = "error message2"
+ err_dict = {'my_bad': error, 'odd_error': error2}
+
+ result = build_general_error_message(err_dict)
+
+ expected = '\n'.join(
+ [('%s: %s' % (k, v)) for k, v in err_dict.iteritems()])
+ self.assertEqual(result, expected)
+
+ def test_with_random_keys_with_errtype(self):
+ """Test build_general_error_message with random keys and errtype."""
+ error = "error message"
+ error2 = "error message2"
+ err_dict = {'my_bad': error, 'odd_error': error2, 'errtype': 'Danger'}
+
+ result = build_general_error_message(err_dict)
+
+ expected = '\n'.join(
+ [('%s: %s' % (k, v)) \
+ for k, v in {'my_bad': error, 'odd_error': error2}.iteritems()])
+ self.assertEqual(result, expected)
+
+ def test_with_not_dict(self):
+ """Test build_general_error_message with argument not dict."""
+ error = "error message"
+ err_dict = Exception(error)
+
+ result = build_general_error_message(err_dict)
+
+ expected = GENERIC_BACKEND_ERROR
+ self.assertEqual(result, expected)
=== modified file 'ubuntu_sso/qt/tests/test_current_user_sign_in_page.py'
--- ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/tests/test_current_user_sign_in_page.py 2012-03-20 16:10:09 +0000
@@ -24,6 +24,7 @@
FakePageUiStyle,
FakeWizardButtonStyle,
)
+from ubuntu_sso.tests import EMAIL
# pylint: disable=W0212
@@ -51,19 +52,32 @@
self.assertTrue(button.properties['default'])
self.assertFalse(button.isEnabled())
+ def test_unicode_in_forgotten_password_link(self):
+ """Ensure that this label supports unicode."""
+ forgot_fr = u"J'ai oublié mon mot de passe"
+ self.patch(gui, "FORGOTTEN_PASSWORD_BUTTON", forgot_fr)
+ forgotten_text = gui.LINK_STYLE.format(link_url='#',
+ link_text=forgot_fr)
+ self.ui._set_translated_strings()
+ self.assertEqual(unicode(self.ui.ui.forgot_password_label.text()),
+ forgotten_text)
+
def test_set_translated_strings(self):
"""Test the translated string method."""
expected = gui.LOGIN_TITLE.format(app_name=self.app_name)
self.assert_title_correct(expected)
expected = gui.LOGIN_SUBTITLE % dict(app_name=self.app_name)
self.assert_subtitle_correct(expected)
- self.assertEqual(self.ui.ui.email_label.text(), gui.EMAIL_LABEL)
- self.assertEqual(self.ui.ui.password_label.text(),
+ self.assertEqual(unicode(self.ui.ui.email_label.text()),
+ gui.EMAIL_LABEL)
+ self.assertEqual(unicode(self.ui.ui.password_label.text()),
gui.LOGIN_PASSWORD_LABEL)
text = gui.LINK_STYLE.format(link_url='#',
link_text=gui.FORGOTTEN_PASSWORD_BUTTON)
- self.assertEqual(self.ui.ui.forgot_password_label.text(), text)
- self.assertEqual(self.ui.ui.sign_in_button.text(), gui.SIGN_IN_BUTTON)
+ self.assertEqual(unicode(self.ui.ui.forgot_password_label.text()),
+ text)
+ self.assertEqual(unicode(self.ui.ui.sign_in_button.text()),
+ gui.SIGN_IN_BUTTON)
def test_connect_ui(self):
"""Test the connect ui method."""
@@ -128,30 +142,27 @@
def test_on_login_error(self):
"""Test the on_login_error method."""
self.patch(self.ui, "show_error", self._set_called)
- app_name = 'my_app'
- self.ui.app_name = app_name
error = {'errtype': 'UserNotValidated'}
- self.ui.on_login_error(app_name, error)
+
+ self.ui.on_login_error(self.app_name, error)
+
self.assertEqual(self._overlay_hide_counter, 0)
self.assertTrue(self.ui.isEnabled())
- expected = ((self.ui, 'my_app', ''), {})
+ expected = ((self.ui, self.app_name, ''), {})
self.assertTrue(expected, self._called)
def test_on_logged_in(self):
"""Test the on_login_in method."""
- email = 'email@example'
- self.ui.ui.email_edit.setText(email)
-
- self.assert_signal_emitted(self.ui.userLoggedIn, (email,),
- self.ui.on_logged_in, self.app_name, email)
+ self.ui.ui.email_edit.setText(EMAIL)
+ self.assert_signal_emitted(self.ui.userLoggedIn, (EMAIL,),
+ self.ui.on_logged_in, self.app_name, EMAIL)
self.assertTrue(self.ui.isEnabled())
def test_on_user_not_validated(self):
"""Test the navigation flow on user not validated."""
- email = 'email@example'
- self.ui.ui.email_edit.setText(email)
- self.assert_signal_emitted(self.ui.userNotValidated, (email,),
- self.ui.on_user_not_validated, self.app_name, email)
+ self.ui.ui.email_edit.setText(EMAIL)
+ self.assert_signal_emitted(self.ui.userNotValidated, (EMAIL,),
+ self.ui.on_user_not_validated, self.app_name, EMAIL)
def test_on_forgotten_password(self):
"""Test the on_forgotten_password method."""
=== modified file 'ubuntu_sso/qt/tests/test_email_verification.py'
--- ubuntu_sso/qt/tests/test_email_verification.py 2012-03-05 18:56:50 +0000
+++ ubuntu_sso/qt/tests/test_email_verification.py 2012-03-20 16:10:09 +0000
@@ -78,14 +78,12 @@
email = 'mail@example'
self.ui.email = email
self.ui.set_titles(email)
- self.assertEqual(self.ui.header.title_label.text(),
- email_verification_page.VERIFY_EMAIL_TITLE)
+ self.assert_title_correct(email_verification_page.VERIFY_EMAIL_TITLE)
expected = email_verification_page.VERIFY_EMAIL_CONTENT % {
"app_name": self.app_name,
"email": email,
}
- self.assertEqual(unicode(self.ui.header.subtitle_label.toolTip()),
- expected)
+ self.assert_subtitle_correct(expected)
def test_initialize_page(self):
"""Test the initialization method."""
@@ -100,10 +98,11 @@
def test_on_email_validation_error(self):
"""Test the validate_email method."""
self.patch(self.ui, "show_error", self._set_called)
- app_name = 'my_app'
error = {email_verification_page: 'error in email_verification_page'}
- self.ui.on_email_validation_error(app_name, error)
- expected = ((self.ui, app_name, ''), {})
+
+ self.ui.on_email_validation_error(self.app_name, error)
+
+ expected = ((self.ui, ''), {})
self.assertTrue(expected, self._called)
self.assertEqual(self._overlay_hide_counter, 1)
=== modified file 'ubuntu_sso/qt/tests/test_enhanced_check_box.py'
--- ubuntu_sso/qt/tests/test_enhanced_check_box.py 2012-02-16 18:40:41 +0000
+++ ubuntu_sso/qt/tests/test_enhanced_check_box.py 2012-03-20 16:10:09 +0000
@@ -16,7 +16,7 @@
"""Tests for the EnhancedCheckBox widget."""
-from PyQt4 import QtCore
+from PyQt4 import QtGui, QtCore
from ubuntu_sso.qt import enhanced_check_box
from ubuntu_sso.qt.tests import BaseTestCase
@@ -40,3 +40,21 @@
check.setText("text")
self.assertEqual(check.text(), "text")
self.assertEqual(check.text(), check.text_label.text())
+
+ def test_enhanced_check_size_adjust_with_small_height(self):
+ """Check if the size of the EnhancedCheckBox is adjusted correctly."""
+ text = 't' * 200
+ height = 10
+ widget = QtGui.QWidget()
+ widget.setFixedSize(200, height)
+ check = enhanced_check_box.EnhancedCheckBox(text, widget)
+ self.assertTrue(check.height() > height)
+
+ def test_enhanced_check_size_adjust_with_big_height(self):
+ """Check if the size of the EnhancedCheckBox is adjusted correctly."""
+ text = 't' * 20
+ height = 200
+ widget = QtGui.QWidget()
+ widget.setFixedSize(200, height)
+ check = enhanced_check_box.EnhancedCheckBox(text, widget)
+ self.assertTrue(check.height() < height)
=== modified file 'ubuntu_sso/qt/tests/test_forgotten_password.py'
--- ubuntu_sso/qt/tests/test_forgotten_password.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/tests/test_forgotten_password.py 2012-03-20 16:10:09 +0000
@@ -75,9 +75,10 @@
subtitle = gui.FORGOTTEN_PASSWORD_SUBTITLE
self.assert_subtitle_correct(subtitle.format(app_name=self.app_name))
- self.assertEqual(self.ui.ui.email_address_label.text(),
+ self.assertEqual(unicode(self.ui.ui.email_address_label.text()),
gui.EMAIL_LABEL)
- self.assertEqual(self.ui.ui.send_button.text(), gui.RESET_PASSWORD)
+ self.assertEqual(unicode(self.ui.ui.send_button.text()),
+ gui.RESET_PASSWORD)
def test_connect_ui(self):
"""Test the connect ui method."""
@@ -112,7 +113,8 @@
"""Test on_password_reset_error method."""
self.patch(self.ui, "show_error", self._set_called)
error = {'errtype': 'FooBarBaz'}
+
self.ui.on_password_reset_error(self.app_name, error)
- expected = ((self.ui, self.app_name,
- gui.REQUEST_PASSWORD_TOKEN_WRONG_EMAIL), {})
+
+ expected = ((self.ui, gui.REQUEST_PASSWORD_TOKEN_WRONG_EMAIL), {})
self.assertTrue(expected, self._called)
=== modified file 'ubuntu_sso/qt/tests/test_loadingoverlay.py'
--- ubuntu_sso/qt/tests/test_loadingoverlay.py 2012-02-23 19:49:02 +0000
+++ ubuntu_sso/qt/tests/test_loadingoverlay.py 2012-03-20 16:10:09 +0000
@@ -29,8 +29,5 @@
def test_status_correct(self):
"""Test if the necessary variables for the animation exists"""
- self.ui.show()
- self.addCleanup(self.ui.hide)
-
self.assertTrue(self.ui.counter is not None)
self.assertTrue(self.ui.orientation is not None)
=== modified file 'ubuntu_sso/qt/tests/test_main.py'
--- ubuntu_sso/qt/tests/test_main.py 2012-02-13 15:43:59 +0000
+++ ubuntu_sso/qt/tests/test_main.py 2012-03-20 16:10:09 +0000
@@ -16,27 +16,123 @@
"""Tests for the main module."""
+from PyQt4 import QtCore
+from twisted.internet import defer
from twisted.trial.unittest import TestCase
from ubuntu_sso.qt import main
+from ubuntu_sso import tests
+
+
+# pylint: disable=C0103
+class FakeUi(object):
+
+ """A fake UI."""
+
+ def size(self):
+ """Fake size."""
+ return QtCore.QSize(100, 100)
+
+ def setGeometry(self, *args):
+ """Fake setGeometry."""
+
+ show = setGeometry
+
+
+class FakeDesktop(object):
+
+ """Fake Desktop Widget."""
+
+ def availableGeometry(self):
+ """Fake availableGeometry for desktop.-"""
+ return QtCore.QRect(100, 100, 100, 100)
+
+
+class FakeApplication(object):
+
+ """Fake QApplication."""
+
+ called = {}
+ __instance = None
+
+ def __init__(self, args):
+ self.called['args'] = args
+ FakeApplication.__instance = self
+
+ def setStyleSheet(self, style):
+ """Fake setStyleSheet."""
+ self.called["setStyleSheet"] = style
+
+ def styleSheet(self):
+ """Fake get style sheet."""
+ return self.called.get("setStyleSheet", '')
+
+ def desktop(self):
+ """Fake Desktop."""
+ return FakeDesktop()
+
+ def exec_(self):
+ """Fake exec_."""
+
+ def exit(self):
+ """Fake exit."""
+
+ @classmethod
+ def instance(cls):
+ """Fake instance."""
+ return FakeApplication.__instance
+# pylint: enable=C0103
class BasicTestCase(TestCase):
"""Test case with a helper tracker."""
+ @defer.inlineCallbacks
+ def setUp(self):
+ yield super(BasicTestCase, self).setUp()
+ self.called = []
+
+ def called_ui(**kw):
+ """record ui call."""
+ self.called.append(('GUI', kw))
+ return FakeUi()
+
+ self.patch(main, 'UbuntuSSOClientGUI', called_ui)
+ self.patch(main.QtGui, 'QApplication', FakeApplication)
+
def test_main(self):
"""Calling main.main() a UI instance is created."""
- called = []
- self.patch(main, 'UbuntuSSOClientGUI',
- lambda **kw: called.append(('GUI', kw)))
- self.patch(main.QtGui.QApplication, 'exec_',
- lambda *a: called.append('main'))
-
+ kwargs = dict(app_name='APP_NAME', foo='foo', bar='bar',
+ baz='yadda', yadda=0)
+ main.main(**kwargs)
+
+ kwargs['close_callback'] = main.QtGui.QApplication.instance().exit
+ self.assertEqual(self.called, [('GUI', kwargs)])
+
+ def test_main_args(self):
+ """Calling main.main() a UI instance is created."""
+ arg_list = (tests.APP_NAME, tests.NAME, tests.PASSWORD,
+ tests.EMAIL_TOKEN)
+ kwargs = dict(app_name=arg_list[0].encode('utf-8'),
+ foo=arg_list[1].encode('utf-8'),
+ bar=arg_list[2].encode('utf-8'),
+ baz=arg_list[3].encode('utf-8'))
+ main.main(**kwargs)
+
+ kwargs['close_callback'] = main.QtGui.QApplication.instance().exit
+ expected = dict(app_name=arg_list[0], foo=arg_list[1],
+ bar=arg_list[2], baz=arg_list[3],
+ close_callback=main.QtGui.QApplication.instance().exit)
+ self.assertEqual(self.called, [('GUI', expected)])
+
+ def test_styles_load(self):
+ """Test that all stylesheets load."""
kwargs = dict(foo='foo', bar='bar', baz='yadda', yadda=0)
main.main(**kwargs)
-
- kwargs['close_callback'] = main.QtGui.QApplication.instance().exit
- self.assertEqual(called, [('GUI', kwargs), 'main'])
-
- test_main.skip = 'Failing with QObject::startTimer: QTimer can only be ' \
- 'used with threads started with QThread'
+ data = []
+ for qss_name in (main.PLATFORM_QSS, ":/stylesheet.qss"):
+ qss = QtCore.QResource(qss_name)
+ data.append(unicode(qss.data()))
+ self.assertEqual(
+ unicode(main.QtGui.QApplication.instance().styleSheet()),
+ '\n'.join(data))
=== modified file 'ubuntu_sso/qt/tests/test_proxy_dialog.py'
--- ubuntu_sso/qt/tests/test_proxy_dialog.py 2012-02-13 16:16:03 +0000
+++ ubuntu_sso/qt/tests/test_proxy_dialog.py 2012-03-20 16:10:09 +0000
@@ -256,7 +256,7 @@
self.patch(dialog, 'done', fake_done)
dialog._on_cancel_clicked()
- self.assertIn(('done', proxy_dialog.USER_CANCELATION), called)
+ self.assertIn(('done', proxy_dialog.USER_CANCELLATION), called)
def assert_save_button(self, set_creds_callback, result_number):
"""Test the save button execution."""
@@ -288,7 +288,7 @@
def test_on_save_clicked_correct(self):
"""Test that we do save the creds."""
set_creds_cb = lambda: defer.succeed(True)
- result_number = proxy_dialog.CREDS_ACQUIRED
+ result_number = proxy_dialog.USER_SUCCESS
self.assert_save_button(set_creds_cb, result_number)
=== modified file 'ubuntu_sso/qt/tests/test_reset_password.py'
--- ubuntu_sso/qt/tests/test_reset_password.py 2012-03-05 18:56:50 +0000
+++ ubuntu_sso/qt/tests/test_reset_password.py 2012-03-20 16:10:09 +0000
@@ -70,27 +70,23 @@
def test_initialize(self):
"""Check the Title and Subtitle."""
- self.ui.show()
self.ui.initializePage()
- self.addCleanup(self.ui.hide)
self.assert_title_correct(RESET_TITLE)
self.assert_subtitle_correct(RESET_SUBTITLE)
- self.assertEqual(self.ui.ui.password_label.text(), PASSWORD1_ENTRY)
- self.assertEqual(self.ui.ui.confirm_password_label.text(),
+ self.assertEqual(unicode(self.ui.ui.password_label.text()),
+ PASSWORD1_ENTRY)
+ self.assertEqual(unicode(self.ui.ui.confirm_password_label.text()),
PASSWORD2_ENTRY)
- self.assertEqual(self.ui.ui.reset_code.text(), RESET_CODE_ENTRY)
+ self.assertEqual(unicode(self.ui.ui.reset_code.text()),
+ RESET_CODE_ENTRY)
def test_focus_changed_password_visibility(self):
"""Check visibility changes when focus_changed() is executed."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.focus_changed(None, self.ui.ui.password_line_edit)
self.assertTrue(self.ui.ui.password_assistance.isVisible())
def test_show_hide_event(self):
"""Check connections to focusChanged on show and hide event."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertEqual(QtGui.QApplication.instance().receivers(
QtCore.SIGNAL("focusChanged(QWidget*, QWidget*)")), 1)
self.ui.hide()
@@ -102,24 +98,20 @@
def test_focus_changed_1(self):
"""Check functions execution when focus_changed() is executed."""
self.patch(common, 'password_default_assistance', self._set_called)
-
- self.ui.show()
- self.addCleanup(self.ui.hide)
-
self.assertFalse(self._called)
+
self.ui.focus_changed(None, self.ui.ui.password_line_edit)
+
self.assertTrue(self.ui.ui.password_assistance.isVisible())
self.assertTrue(self._called)
def test_focus_changed_2(self):
"""Check functions execution when focus_changed() is executed."""
self.patch(common, 'password_check_match', self._set_called)
-
- self.ui.show()
- self.addCleanup(self.ui.hide)
-
self.assertFalse(self._called)
+
self.ui.focus_changed(None, self.ui.ui.confirm_password_line_edit)
+
self.assertTrue(self.ui.ui.password_assistance.isVisible())
self.assertTrue(self._called)
=== modified file 'ubuntu_sso/qt/tests/test_setup_account.py'
--- ubuntu_sso/qt/tests/test_setup_account.py 2012-03-06 14:13:10 +0000
+++ ubuntu_sso/qt/tests/test_setup_account.py 2012-03-20 16:10:09 +0000
@@ -43,13 +43,20 @@
"""
self.ui.ui.name_edit.setText("")
self.ui.name_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertTrue(self.ui.ui.name_assistance.isVisible())
- self.assertEqual(
- unicode(self.ui.ui.name_assistance.text()),
- gui.ERROR_STYLE % gui.EMPTY_NAME)
- self.ui.hide()
+ self.assert_error_correct(self.ui.ui.name_assistance, gui.EMPTY_NAME,
+ max_width=self.ui.header.max_title_width)
+
+ def test_hide_error_on_refresh_clicked(self):
+ """Hide form errors when the user click to refresh the captcha."""
+ self.ui.show_error('error')
+ self.assert_error_correct(self.ui.form_errors_label, 'error',
+ max_width=self.ui.header.max_title_width)
+
+ self.ui.ui.refresh_label.linkActivated.emit('error')
+
+ message = unicode(self.ui.form_errors_label.text())
+ self.assertEqual(message, ' ')
def test_enable_setup_button_with_visible_check(self):
"""Test _enable_setup_button method with terms check visible."""
@@ -65,8 +72,6 @@
self.ui.ui.captcha_solution_edit.setText('captcha solution')
self.ui.terms_checkbox.setChecked(True)
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.terms_checkbox.setVisible(True)
self.ui.ui.captcha_solution_edit.textEdited.emit('')
self.assertTrue(self.ui.set_up_button.isEnabled())
@@ -84,8 +89,6 @@
self.ui.ui.confirm_password_edit.setText(password)
self.ui.ui.captcha_solution_edit.setText('captcha solution')
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.terms_checkbox.setVisible(False)
self.ui.ui.captcha_solution_edit.textEdited.emit('')
self.assertTrue(self.ui.set_up_button.isEnabled())
@@ -114,8 +117,6 @@
def test_password_focus_gain(self):
"""Check functions execution when focus_changed() is executed."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.ui.password_assistance.setVisible(False)
self.assertFalse(self.ui.ui.password_assistance.isVisible())
self.patch(self.ui, 'name_assistance', self._set_called)
@@ -174,7 +175,6 @@
"""Test on_user_registered method."""
email = 'email@example'
self.ui.ui.email_edit.setText(email)
-
self.assert_signal_emitted(self.ui.userRegistered, (email,),
self.ui.on_user_registered, self.app_name, email)
@@ -193,8 +193,6 @@
def test_initialize_page(self):
"""Widgets are properly initialized."""
self.ui.initializePage()
- self.ui.show()
- self.addCleanup(self.ui.hide)
# set up account button
expected = [QtGui.QWizard.BackButton, QtGui.QWizard.Stretch,
@@ -210,12 +208,15 @@
self.assertFalse(self.ui.captcha_received)
# labels
- self.assertEqual(self.ui.ui.name_label.text(), gui.NAME_ENTRY)
- self.assertEqual(self.ui.ui.email_label.text(), gui.EMAIL)
- self.assertEqual(self.ui.ui.confirm_email_label.text(),
+ self.assertEqual(unicode(self.ui.ui.name_label.text()),
+ gui.NAME_ENTRY)
+ self.assertEqual(unicode(self.ui.ui.email_label.text()),
+ gui.EMAIL)
+ self.assertEqual(unicode(self.ui.ui.confirm_email_label.text()),
gui.RETYPE_EMAIL)
- self.assertEqual(self.ui.ui.password_label.text(), gui.PASSWORD)
- self.assertEqual(self.ui.ui.confirm_password_label.text(),
+ self.assertEqual(unicode(self.ui.ui.password_label.text()),
+ gui.PASSWORD)
+ self.assertEqual(unicode(self.ui.ui.confirm_password_label.text()),
gui.RETYPE_PASSWORD)
# assistants
@@ -230,8 +231,6 @@
self.patch(self.ui, 'set_next_validation', self._set_called)
self.ui.initializePage()
self.ui.captcha_received = True
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.set_up_button.clicked.emit(False)
self.assertEqual(self._called, ((False,), {}))
@@ -239,13 +238,10 @@
def test_set_error_message(self):
"""Check the state of the label after calling: set_error_message."""
self.ui.email_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.set_error_message(self.ui.ui.email_assistance, "message")
self.assertTrue(self.ui.ui.email_assistance.isVisible())
- self.assertEqual(
- unicode(self.ui.ui.email_assistance.text()),
- gui.ERROR_STYLE % "message")
+ self.assert_error_correct(self.ui.ui.email_assistance, "message",
+ max_width=self.ui.header.max_title_width)
def test_blank_name(self):
"""Status when the name field is blank (spaces).
@@ -255,13 +251,9 @@
"""
self.ui.ui.name_edit.setText(" ")
self.ui.name_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertTrue(self.ui.ui.name_assistance.isVisible())
- self.assertEqual(
- unicode(self.ui.ui.name_assistance.text()),
- gui.ERROR_STYLE % gui.EMPTY_NAME)
- self.ui.hide()
+ self.assert_error_correct(self.ui.ui.name_assistance, gui.EMPTY_NAME,
+ max_width=self.ui.header.max_title_width)
def test_valid_name(self):
"""Status when the name field is valid.
@@ -270,10 +262,7 @@
"""
self.ui.ui.name_edit.setText("John Doe")
self.ui.name_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertFalse(self.ui.ui.name_assistance.isVisible())
- self.ui.hide()
def test_invalid_email(self):
"""Status when the email field has no @.
@@ -283,12 +272,10 @@
"""
self.ui.ui.email_edit.setText("foobar")
self.ui.email_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertTrue(self.ui.ui.email_assistance.isVisible())
- self.assertEqual(
- unicode(self.ui.ui.email_assistance.text()),
- gui.ERROR_STYLE % gui.INVALID_EMAIL)
+ self.assert_error_correct(self.ui.ui.email_assistance,
+ gui.INVALID_EMAIL,
+ max_width=self.ui.header.max_title_width)
def test_valid_email(self):
"""Status when the email field has a @.
@@ -297,10 +284,7 @@
"""
self.ui.ui.email_edit.setText("foo@bar")
self.ui.email_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertFalse(self.ui.ui.email_assistance.isVisible())
- self.ui.hide()
def test_matching_emails(self):
"""Status when the email fields match.
@@ -310,10 +294,7 @@
self.ui.ui.email_edit.setText("foo@bar")
self.ui.ui.confirm_email_edit.setText("foo@bar")
self.ui.confirm_email_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertFalse(self.ui.ui.confirm_email_assistance.isVisible())
- self.ui.hide()
def test_not_matching_emails(self):
"""Status when the email fields don't match.
@@ -324,18 +305,13 @@
self.ui.ui.email_edit.setText("foo@bar")
self.ui.ui.confirm_email_edit.setText("foo@baz")
self.ui.confirm_email_assistance()
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.assertTrue(self.ui.ui.confirm_email_assistance.isVisible())
- self.assertEqual(
- unicode(self.ui.ui.confirm_email_assistance.text()),
- gui.ERROR_STYLE % gui.EMAIL_MATCH)
- self.ui.hide()
+ self.assert_error_correct(self.ui.ui.confirm_email_assistance,
+ gui.EMAIL_MATCH,
+ max_width=self.ui.header.max_title_width)
def test_focus_changed_password_visibility(self):
"""Check visibility changes when focus_changed() is executed."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
self.ui.focus_changed(None, self.ui.ui.password_edit)
self.assertTrue(self.ui.ui.password_assistance.isVisible())
@@ -350,33 +326,41 @@
self.ui.showEvent(QtGui.QShowEvent())
self.ui.hideEvent(QtGui.QHideEvent())
- def test_on_captcha_refreshing(self):
+ def test_on_captcha_refreshing_visible(self):
"""Check the state of the overlay on captcha refreshing."""
- self.assertEqual(self._overlay_show_counter, 0)
- self.ui.on_captcha_refreshing()
- self.assertEqual(self._overlay_show_counter, 0)
- self.assertTrue(self.ui.isEnabled())
- self.ui.captcha_received = True
- self.ui.show()
- self.addCleanup(self.ui.hide)
- self.assertEqual(self._overlay_show_counter, 0)
- self.assertTrue(self.ui.isEnabled())
- self.ui.on_captcha_refreshing()
+ self.ui.hide_overlay()
+
+ self.assertEqual(self._overlay_show_counter, 0)
+ self.assertTrue(self.ui.isEnabled())
+
+ self.ui.on_captcha_refreshing()
+
self.assertFalse(self.ui.isEnabled())
self.assertEqual(self._overlay_show_counter, 1)
+ def test_on_captcha_refreshing_not_visible(self):
+ """Check the state of the overlay on captcha refreshing."""
+ self.ui.hide_overlay()
+
+ self.assertEqual(self._overlay_show_counter, 0)
+ self.assertTrue(self.ui.isEnabled())
+
+ self.ui.hide()
+ self.ui.on_captcha_refreshing()
+
+ self.assertEqual(self._overlay_show_counter, 0)
+ self.assertTrue(self.ui.isEnabled())
+
def test_on_captcha_refresh_complete(self):
"""Check the state of the overlay on captcha refreshing complete."""
self.assertEqual(self._overlay_hide_counter, 0)
- self.assertTrue(self.ui.isEnabled())
+
self.ui.on_captcha_refresh_complete()
+
self.assertEqual(self._overlay_hide_counter, 1)
- self.assertTrue(self.ui.isEnabled())
def test_hide_error_on_refresh_captcha(self):
"""Test that the errors are hidden on refresh captcha."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
- self.ui.show_error(self.app_name, 'error-message')
+ self.ui.show_error('error-message')
self.ui.ui.refresh_label.linkActivated.emit('link')
self.assertEqual(self.ui.form_errors_label.text(), ' ')
=== modified file 'ubuntu_sso/qt/tests/test_ssl_dialog.py'
--- ubuntu_sso/qt/tests/test_ssl_dialog.py 2012-03-05 16:32:37 +0000
+++ ubuntu_sso/qt/tests/test_ssl_dialog.py 2012-03-20 16:10:09 +0000
@@ -142,7 +142,8 @@
def test_set_expander(self):
"""Test that the expander is correctly set."""
- self.assertEqual(SSL_CERT_DETAILS, self.dialog.expander.text())
+ self.assertEqual(SSL_CERT_DETAILS,
+ unicode(self.dialog.expander.text()))
self.assertNotEqual(None, self.dialog.expander.content)
self.assertEqual(2, self.dialog.ui.expander_layout.indexOf(
self.dialog.expander))
=== modified file 'ubuntu_sso/qt/tests/test_sso_wizard_page.py'
--- ubuntu_sso/qt/tests/test_sso_wizard_page.py 2012-03-05 18:56:50 +0000
+++ ubuntu_sso/qt/tests/test_sso_wizard_page.py 2012-03-20 16:10:09 +0000
@@ -16,98 +16,137 @@
"""Test the SSOWizardPage and related."""
-from twisted.internet import defer
-
-from ubuntu_sso.qt import PREFERED_UI_SIZE
-from ubuntu_sso.qt.setup_account_page import SetupAccountPage
-from ubuntu_sso.qt.sso_wizard_page import Header
-from ubuntu_sso.qt.tests import BaseTestCase, PageBaseTestCase
-
-
-class HeaderTest(BaseTestCase):
+from ubuntu_sso.qt import PREFERED_UI_SIZE, sso_wizard_page as gui
+from ubuntu_sso.qt.tests import (
+ APP_NAME,
+ BaseTestCase,
+ build_string_for_pixels,
+ PageBaseTestCase,
+)
+
+
+MAX_WIDTH = 100
+
+
+class WizardHeaderTestCase(BaseTestCase):
"""Tests for injected Header in each Wizard Page."""
- @defer.inlineCallbacks
- def setUp(self):
- yield super(HeaderTest, self).setUp()
- self.header = Header()
+ kwargs = dict(max_width=MAX_WIDTH)
+ ui_class = gui.WizardHeader
+ ui_wizard_class = None
def test_label_state(self):
"""Check the title and subtitle properties."""
- self.assertTrue(self.header.title_label.wordWrap())
- self.assertTrue(self.header.subtitle_label.wordWrap())
- self.assertFalse(self.header.title_label.isVisible())
- self.assertFalse(self.header.subtitle_label.isVisible())
+ self.assertTrue(self.ui.title_label.wordWrap())
+ self.assertTrue(self.ui.subtitle_label.wordWrap())
+ self.assertFalse(self.ui.title_label.isVisible())
+ self.assertFalse(self.ui.subtitle_label.isVisible())
def test_set_title(self):
"""Check if set_title works ok, showing the widget if necessary."""
- self.header.set_title('title')
- self.assertEqual(self.header.title_label.text(), 'title')
- self.header.show()
- self.assertTrue(self.header.title_label.isVisible())
- self.header.hide()
+ max_width = self.ui.max_title_width
+ text = build_string_for_pixels(self.ui.title_label, max_width)
+
+ self.ui.set_title(text)
+
+ self.assert_title_correct(self.ui.title_label, text, max_width)
def test_set_elided_title(self):
"""Check if set_title adds the ellipsis when necessary."""
# add an extra letter so we ensure this needs to be trimmed
- title = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a'
- self.header.set_title(title)
- self.assertEqual(self.header.title_label.toolTip(), title)
- expected = unicode(self.header.title_label.text())
- self.assertTrue(expected.endswith(u'\u2026'))
+ max_width = self.ui.max_title_width
+ text = build_string_for_pixels(self.ui.title_label, max_width + 10)
+
+ self.ui.set_title(text)
+
+ self.assert_title_correct(self.ui.title_label, text, max_width)
def test_set_empty_title(self):
"""Check if the widget is hidden for empty title."""
- self.header.set_title('')
- self.assertFalse(self.header.title_label.isVisible())
+ self.ui.set_title('')
+
+ self.assertEqual(self.ui.title_label.toolTip(), '')
+ self.assertFalse(self.ui.title_label.isVisible())
def test_set_subtitle(self):
"""Check if set_subtitle works ok, showing the widget if necessary."""
- self.header.set_subtitle('subtitle')
- self.assertEqual(self.header.subtitle_label.text(), 'subtitle')
- self.header.show()
- self.assertTrue(self.header.subtitle_label.isVisible())
- self.header.hide()
+ max_width = self.ui.max_subtitle_width
+ text = build_string_for_pixels(self.ui.subtitle_label, max_width)
+
+ self.ui.set_subtitle(text)
+
+ self.assert_subtitle_correct(self.ui.subtitle_label, text, max_width)
def test_set_elided_subtitle(self):
"""Check if set_subtitle adds the ellipsis when necessary."""
- subtitle = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a'
- self.header.set_subtitle(subtitle)
- self.assertEqual(self.header.subtitle_label.toolTip(), subtitle)
- expected = unicode(self.header.subtitle_label.text())
- self.assertTrue(expected.endswith(u'\u2026'))
+ max_width = self.ui.max_subtitle_width
+ text = build_string_for_pixels(self.ui.subtitle_label, max_width + 10)
+
+ self.ui.set_subtitle(text)
+
+ self.assert_subtitle_correct(self.ui.subtitle_label, text, max_width)
def test_set_empty_subtitle(self):
"""Check if the widget is hidden for empty subtitle."""
- self.header.set_title('')
- self.assertFalse(self.header.title_label.isVisible())
-
-
-class SSOWizardPageTest(PageBaseTestCase):
-
- """Tests for SSOWizardPage."""
-
- ui_class = SetupAccountPage
+ self.ui.set_subtitle('')
+
+ self.assertEqual(self.ui.subtitle_label.toolTip(), '')
+ self.assertFalse(self.ui.subtitle_label.isVisible())
+
+
+class BaseWizardPageTestCase(PageBaseTestCase):
+
+ """Tests for SSOWizardPage."""
+
+ kwargs = {}
+ ui_class = gui.BaseWizardPage
+
+ def test_max_width(self):
+ """The max_width is correct."""
+ self.assertEqual(self.ui.max_width, 0)
+
+
+class SSOWizardPageTestCase(BaseWizardPageTestCase):
+
+ """Tests for SSOWizardPage."""
+
+ kwargs = dict(app_name=APP_NAME)
+ ui_class = gui.SSOWizardPage
+
+ def test_max_width(self):
+ """The max_width is correct."""
+ self.assertEqual(self.ui.max_width, PREFERED_UI_SIZE['width'])
def test_show_error(self):
"""Test show_error with a normal length string."""
message = 'error-message'
- self.ui.show_error(self.app_name, message)
- self.assertEqual(self.ui.form_errors_label.toolTip(), message)
- expected = unicode(self.ui.form_errors_label.text())
- self.assertEqual(expected, message)
+ self.ui.show_error(message)
+
+ self.assert_error_correct(self.ui.form_errors_label, message,
+ self.ui.header.max_title_width)
def test_show_error_long_text(self):
"""Test show_error with a long length string."""
- message = 'a' * int(PREFERED_UI_SIZE['width'] * 0.95) + 'a'
- self.ui.show_error(self.app_name, message)
- self.assertEqual(self.ui.form_errors_label.toolTip(), message)
- expected = unicode(self.ui.form_errors_label.text())
- self.assertTrue(expected.endswith(u'\u2026'))
+ message = build_string_for_pixels(self.ui.form_errors_label,
+ self.ui.header.max_title_width + 10)
+
+ self.ui.show_error(message)
+ self.assert_error_correct(self.ui.form_errors_label, message,
+ self.ui.header.max_title_width)
def test_hide_error(self):
"""Test show_error with a long length string."""
- message = ' '
self.ui.hide_error()
- self.assertEqual(self.ui.form_errors_label.text(), message)
+
+ self.assertEqual(self.ui.form_errors_label.text(), ' ')
+
+ def test_setup_page_with_failing_backend(self):
+ """Test how the ui react with an invalid backend."""
+ self.patch(gui.main, "get_sso_client", lambda: None)
+ self.patch(self.ui, "show_error", self._set_called)
+ self.ui.setup_page()
+ reason = 'There was a problem trying to setup the page %r' % self.ui
+ expected = ((reason,), {})
+ self.assertEqual(expected, self._called)
+ self.assertFalse(self.ui.isEnabled())
=== modified file 'ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py'
--- ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-05 14:37:43 +0000
+++ ubuntu_sso/qt/tests/test_ubuntu_sso_wizard.py 2012-03-20 16:10:09 +0000
@@ -19,6 +19,11 @@
from PyQt4 import QtGui
from twisted.internet import defer
+from ubuntu_sso.tests import (
+ APP_NAME,
+ EMAIL,
+ PASSWORD,
+)
from ubuntu_sso.qt import PREFERED_UI_SIZE, ubuntu_sso_wizard
from ubuntu_sso.qt.tests import (
BaseTestCase,
@@ -86,6 +91,11 @@
finish_button.clicked.emit(True)
self.assertEqual(self._called, ((None,), {}))
+ def test_window_title(self):
+ """Check the window title for the application."""
+ title = unicode(self.ui.windowTitle())
+ self.assertEqual(title, ubuntu_sso_wizard.WINDOW_TITLE)
+
class UbuntuSSOWizardTestCase(BaseTestCase):
@@ -102,13 +112,9 @@
def test_window_size(self):
"""check the window size."""
- self.ui.show()
- self.addCleanup(self.ui.hide)
size = self.ui.size()
- # pylint: disable=E1101
- self.assertGreaterEqual(size.height(), PREFERED_UI_SIZE['height'])
- self.assertGreaterEqual(size.width(), PREFERED_UI_SIZE['width'])
- # pylint: enable=E1101
+ self.assertTrue(size.height() >= PREFERED_UI_SIZE['height'])
+ self.assertTrue(size.width() >= PREFERED_UI_SIZE['width'])
def test_move_to_success_page_state(self):
"""Test _move_to_success_page method."""
@@ -123,6 +129,9 @@
def test_overlay_shows(self):
"""Test if the signals call the overlay.show properly."""
+ # reset the counter
+ self.ui.overlay.show_counter = 0
+
for page in self.ui._pages:
page.show_overlay()
@@ -130,6 +139,9 @@
def test_overlay_hides(self):
"""Test if the signals call the overlay.show properly."""
+ # reset the counter
+ self.ui.overlay.show_counter = 0
+
for page in self.ui._pages:
page.hide_overlay()
@@ -146,3 +158,21 @@
self.ui.reset_password.passwordChanged.emit('')
expected = ((self.ui.current_user,), {})
self.assertEqual(expected, self._called)
+
+ def test_email_verification_page_params_from_current_user(self):
+ """Tests that email_verification_page receives the proper params."""
+ self.ui._next_id = self.ui.current_user_page_id
+ self.ui.next()
+ self.ui.current_user.ui.email_edit.setText(EMAIL)
+ self.ui.current_user.ui.password_edit.setText(PASSWORD)
+ self.ui.current_user.on_user_not_validated(APP_NAME, EMAIL)
+ self.assertEqual(EMAIL, self.ui.email_verification.email)
+ self.assertEqual(PASSWORD, self.ui.email_verification.password)
+
+ def test_email_verification_page_params_from_setup(self):
+ """Tests that email_verification_page receives the proper params."""
+ self.ui.setup_account.ui.email_edit.setText(EMAIL)
+ self.ui.setup_account.ui.password_edit.setText(PASSWORD)
+ self.ui.setup_account.on_user_registered(APP_NAME, {})
+ self.assertEqual(EMAIL, self.ui.email_verification.email)
+ self.assertEqual(PASSWORD, self.ui.email_verification.password)
=== modified file 'ubuntu_sso/qt/ubuntu_sso_wizard.py'
--- ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-05 20:30:57 +0000
+++ ubuntu_sso/qt/ubuntu_sso_wizard.py 2012-03-20 16:10:09 +0000
@@ -30,7 +30,7 @@
USER_SUCCESS,
)
from ubuntu_sso.logger import setup_gui_logging
-from ubuntu_sso.qt import PREFERED_UI_SIZE
+from ubuntu_sso.qt import PREFERED_UI_SIZE, WINDOW_TITLE
from ubuntu_sso.qt.current_user_sign_in_page import CurrentUserSignInPage
from ubuntu_sso.qt.email_verification_page import EmailVerificationPage
from ubuntu_sso.qt.error_page import ErrorPage
@@ -141,6 +141,8 @@
def _go_back_to_page(self, page):
"""Move back until it reaches the 'page'."""
+ logger.debug('Moving back from page: %s, to page: %s',
+ self.currentPage(), page)
page_id = self._pages[page]
visited_pages = self.visitedPages()
for index in reversed(visited_pages):
@@ -150,30 +152,42 @@
def _move_to_reset_password_page(self):
"""Move to the reset password page wizard."""
+ logger.debug('Moving to ResetPasswordPage from: %s',
+ self.currentPage())
self._next_id = self.reset_password_page_id
self.next()
self._next_id = -1
- def _move_to_email_verification_page(self):
+ def _move_to_email_verification_page(self, email):
"""Move to the email verification page wizard."""
+ logger.debug('Moving to EmailVerificationPage from: %s',
+ self.currentPage())
self._next_id = self.email_verification_page_id
+ self.email_verification.email = unicode(email)
+ self.email_verification.password = self.currentPage().password
self.next()
self._next_id = -1
def _move_to_setup_account_page(self):
"""Move to the setup account page wizard."""
+ logger.debug('Moving to SetupAccountPage from: %s',
+ self.currentPage())
self._next_id = self.setup_account_page_id
self.next()
self._next_id = -1
def _move_to_login_page(self):
"""Move to the login page wizard."""
+ logger.debug('Moving to CurrentUserSignInPage from: %s',
+ self.currentPage())
self._next_id = self.current_user_page_id
self.next()
self._next_id = -1
def _move_to_success_page(self):
"""Move to the success page wizard."""
+ logger.debug('Moving to SuccessPage from: %s',
+ self.currentPage())
self._next_id = self.success_page_id
self.next()
self.setButtonLayout([
@@ -186,6 +200,8 @@
def _move_to_forgotten_page(self):
"""Move to the forgotten page wizard."""
+ logger.debug('Moving to ForgottenPasswordPage from: %s',
+ self.currentPage())
self._next_id = self.forgotten_password_page_id
self.next()
self._next_id = -1
@@ -258,6 +274,7 @@
logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.',
app_name, kwargs)
self.app_name = app_name
+ self.setWindowTitle(WINDOW_TITLE)
# create the controller and the ui, then set the cb and call the show
# method so that we can work
self.wizard = UbuntuSSOWizard(app_name=app_name, **kwargs)
=== modified file 'ubuntu_sso/utils/__init__.py'
--- ubuntu_sso/utils/__init__.py 2012-02-17 20:48:27 +0000
+++ ubuntu_sso/utils/__init__.py 2012-03-20 16:10:09 +0000
@@ -32,8 +32,14 @@
logger = setup_logging("ubuntu_sso.utils")
+BIN_SUFFIX = 'bin'
DATA_SUFFIX = 'data'
-BIN_SUFFIX = 'bin'
+
+if sys.platform == "win32":
+ from ubuntu_sso.utils import windows as source
+else:
+ from ubuntu_sso.utils import linux as source
+PLATFORM_QSS = source.PLATFORM_QSS
def _get_dir(dir_name, dir_constant):
@@ -88,8 +94,15 @@
found, return the value of the BIN_DIR.
"""
- result = _get_dir(dir_name=BIN_SUFFIX, dir_constant='BIN_DIR')
+ # If sys is frozen, this is an .exe, and all binaries are in
+ # the same place
+ if getattr(sys, "frozen", None) is not None:
+ exec_path = os.path.abspath(sys.executable)
+ result = os.path.dirname(exec_path)
+ else:
+ result = _get_dir(dir_name=BIN_SUFFIX, dir_constant='BIN_DIR')
assert result is not None, '%r dir can not be None.' % BIN_SUFFIX
+ logger.info('get_bin_dir: returning dir located at %r.', result)
return result
=== added file 'ubuntu_sso/utils/linux.py'
--- ubuntu_sso/utils/linux.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/linux.py 2012-03-20 16:10:09 +0000
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2010-2012 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see .
+
+"""Platform specific constants and functions (for Linux)."""
+
+PLATFORM_QSS = ":/linux.qss"
=== modified file 'ubuntu_sso/utils/runner/glib.py'
--- ubuntu_sso/utils/runner/glib.py 2012-02-17 18:43:17 +0000
+++ ubuntu_sso/utils/runner/glib.py 2012-03-20 16:10:09 +0000
@@ -27,7 +27,7 @@
logger = setup_logging("ubuntu_sso.utils.runner.glib")
-NO_SUCH_FILE_OR_DIR = 'No such file or directory'
+NO_SUCH_FILE_OR_DIR = '[Errno 2]'
def spawn_program(args, reply_handler, error_handler):
=== modified file 'ubuntu_sso/utils/runner/tx.py'
--- ubuntu_sso/utils/runner/tx.py 2012-02-17 19:13:15 +0000
+++ ubuntu_sso/utils/runner/tx.py 2012-03-20 16:10:09 +0000
@@ -26,7 +26,7 @@
logger = setup_logging("ubuntu_sso.utils.runner.tx")
-NO_SUCH_FILE_OR_DIR = 'OSError: [Errno 2] No such file or directory'
+NO_SUCH_FILE_OR_DIR = 'OSError: [Errno 2]'
EXE_EXT = ''
=== modified file 'ubuntu_sso/utils/tests/test_common.py'
--- ubuntu_sso/utils/tests/test_common.py 2012-02-15 20:21:52 +0000
+++ ubuntu_sso/utils/tests/test_common.py 2012-03-20 16:10:09 +0000
@@ -17,6 +17,7 @@
"""Tests for the oauth_headers helper function."""
import logging
+import os
import sys
import time
@@ -146,13 +147,21 @@
self.assertEqual(expected, result)
-class GetBinDirTestCase(TestCase):
+class GetBinDirTestCase(GetProjectDirTestCase):
"""Test case for get_bin_dir when constants module is not defined."""
DIR_NAME = utils.BIN_SUFFIX
DIR_CONSTANT = 'BIN_DIR'
DIR_GETTER = 'get_bin_dir'
+ def test_frozen_binary(self):
+ """Test that frozen binaries return a valid path."""
+ sys.frozen = True
+ self.addCleanup(delattr, sys, "frozen")
+ expected = os.path.dirname(os.path.abspath(sys.executable))
+ result = self.get_dir()
+ self.assertEqual(expected, result)
+
class GetBinDirWithConstantsTestCase(GetProjectDirWithConstantsTestCase):
"""Test case for get_bin_dir when constants module is not defined."""
=== modified file 'ubuntu_sso/utils/ui.py'
--- ubuntu_sso/utils/ui.py 2012-03-06 14:13:10 +0000
+++ ubuntu_sso/utils/ui.py 2012-03-20 16:10:09 +0000
@@ -43,7 +43,7 @@
CAPTCHA_RELOAD_TEXT = _('refresh')
CAPTCHA_RELOAD_TOOLTIP = _('Reload')
CAPTCHA_REQUIRED_ERROR = _('The captcha is a required field')
-CLOSE_AND_SETUP_LATER = _('Close window and setup later')
+CLOSE_AND_SETUP_LATER = _('Close window and set up later')
CONGRATULATIONS = _("Congratulations, {app_name} is installed!")
CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s enter your '
'details below.')
@@ -133,6 +133,12 @@
SSL_CERT_DETAILS = _('Certificate details')
SSL_CONNECT_BUTTON = _('Connect')
SSL_DETAILS_HELP = _('the details ssl certificate we are going to show.')
+SSL_DETAILS_TEMPLATE = ('Organization:\t%(organization)s\n'
+ 'Common Name:\t%(common_name)s\n'
+ 'Locality Name:\t%(locality_name)s\n'
+ 'Unit:\t%(unit)s\n'
+ 'Country:\t%(country_name)s\n'
+ 'State or Province:\t%(state_name)s')
SSL_DESCRIPTION = _('Open the SSL certificate UI.')
SSL_DIALOG_TITLE = _('SSL Certificate Not Valid')
SSL_DOMAIN_HELP = _('the domain whose ssl certificate we are going to show.')
=== modified file 'ubuntu_sso/utils/webclient/__init__.py'
--- ubuntu_sso/utils/webclient/__init__.py 2012-02-09 18:11:02 +0000
+++ ubuntu_sso/utils/webclient/__init__.py 2012-03-20 16:10:09 +0000
@@ -19,6 +19,7 @@
# pylint: disable=W0611
from ubuntu_sso.utils.webclient.common import (
+ ProxyUnauthorizedError,
UnauthorizedError,
WebClientError,
)
@@ -26,8 +27,23 @@
def is_qt4reactor_installed():
"""Check if the qt4reactor is installed."""
- reactor = sys.modules.get("twisted.internet.reactor")
- return reactor and getattr(reactor, "qApp", None)
+ result = False
+
+ if not 'PyQt4' in sys.modules:
+ return result
+
+ try:
+ from PyQt4.QtCore import QCoreApplication
+ from PyQt4.QtGui import QApplication
+
+ # we could be running a process with or without ui, and those are diff
+ # apps.
+ result = (QCoreApplication.instance() is not None
+ or QApplication.instance() is not None)
+ except ImportError:
+ pass
+
+ return result
def webclient_module():
=== modified file 'ubuntu_sso/utils/webclient/common.py'
--- ubuntu_sso/utils/webclient/common.py 2012-02-09 18:11:02 +0000
+++ ubuntu_sso/utils/webclient/common.py 2012-03-20 16:10:09 +0000
@@ -17,14 +17,23 @@
import cgi
import collections
+import os
from httplib2 import iri2uri
from oauth import oauth
from twisted.internet import defer
from urlparse import urlparse
+from ubuntu_sso import USER_SUCCESS, UI_PROXY_CREDS_DIALOG
+from ubuntu_sso.logger import setup_logging
+from ubuntu_sso.utils.runner import spawn_program
+from ubuntu_sso.utils.ui import SSL_DETAILS_TEMPLATE
from ubuntu_sso.utils.webclient.timestamp import TimestampChecker
+SSL_DIALOG = 'ubuntu-sso-ssl-certificate-qt'
+
+logger = setup_logging("ubuntu_sso.utils.webclient.common")
+
class WebClientError(Exception):
"""An http error happened while calling the webservice."""
@@ -34,8 +43,12 @@
"""The request ended with bad_request, unauthorized or forbidden."""
+class ProxyUnauthorizedError(WebClientError):
+ """Failure raised when there is an issue with the proxy auth."""
+
+
class Response(object):
- """A reponse object."""
+ """A response object."""
def __init__(self, content, headers=None):
"""Initialize this instance."""
@@ -77,10 +90,14 @@
timestamp_checker = None
- def __init__(self, username=None, password=None, oauth_sign_plain=False):
+ def __init__(self, appname='', username=None, password=None,
+ oauth_sign_plain=False):
"""Initialize this instance."""
+ self.appname = appname
self.username = username
self.password = password
+ self.proxy_username = None
+ self.proxy_password = None
self.oauth_sign_plain = oauth_sign_plain
def request(self, iri, method="GET", extra_headers=None,
@@ -154,3 +171,94 @@
def shutdown(self):
"""Shut down all pending requests (if possible)."""
+
+ @defer.inlineCallbacks
+ def _load_proxy_creds_from_keyring(self, domain):
+ """Load the proxy creds from the keyring."""
+ from ubuntu_sso.keyring import Keyring
+ keyring = Keyring()
+ try:
+ creds = yield keyring.get_credentials(str(domain))
+ logger.debug('Got credentials from keyring.')
+ except Exception, e:
+ logger.error('Error when retrieving the creds.')
+ raise WebClientError('Error when retrieving the creds.', e)
+ if creds is not None:
+ # if we are loading the same creds it means that we got the wrong
+ # ones
+ if (self.proxy_username == creds['username'] and
+ self.proxy_password == creds['password']):
+ defer.returnValue(False)
+ else:
+ self.proxy_username = creds['username']
+ self.proxy_password = creds['password']
+ defer.returnValue(True)
+ logger.debug('Proxy creds not in keyring.')
+ defer.returnValue(False)
+
+ def _launch_proxy_creds_dialog(self, domain, retry):
+ """Launch the dialog used to get the creds."""
+ from ubuntu_sso.utils import get_bin_dir
+
+ bin_dir = get_bin_dir()
+ args = (os.path.join(bin_dir, UI_PROXY_CREDS_DIALOG), '--domain',
+ domain)
+ if retry:
+ args += ('--retry',)
+ return spawn_program(args)
+
+ @defer.inlineCallbacks
+ def request_proxy_auth_credentials(self, domain, retry):
+ """Request the auth creds to the user."""
+ if not retry:
+ if (self.proxy_username is not None
+ and self.proxy_password is not None):
+ logger.debug('Not retry and credentials are present.')
+ defer.returnValue(True)
+ else:
+ creds_loaded = yield self._load_proxy_creds_from_keyring(
+ domain)
+ if creds_loaded:
+ defer.returnValue(True)
+
+ try:
+ return_code = yield self._launch_proxy_creds_dialog(domain, retry)
+ except Exception, e:
+ logger.error('Error when running external ui process.')
+ raise WebClientError('Error when running external ui process.', e)
+
+ if return_code == USER_SUCCESS:
+ creds_loaded = yield self._load_proxy_creds_from_keyring(domain)
+ defer.returnValue(creds_loaded)
+ else:
+ logger.debug('Could not retrieve the credentials. Return code: %r',
+ return_code)
+ defer.returnValue(False)
+
+ def format_ssl_details(self, details):
+ """Return a formatted string with the details."""
+ return SSL_DETAILS_TEMPLATE % details
+
+ def _launch_ssl_dialog(self, domain, details):
+ """Launch a dialog used to approve the ssl cert."""
+ from ubuntu_sso.utils import get_bin_dir
+
+ bin_dir = get_bin_dir()
+ args = (os.path.join(bin_dir, SSL_DIALOG), '--domain', domain,
+ '--details', details,
+ '--appname', self.appname)
+ return spawn_program(args)
+
+ def _was_ssl_accepted(self, cert_details):
+ """Return if the cert was already accepted."""
+ # TODO: Ensure that we look at pinned certs in a following branch
+ return False
+
+ @defer.inlineCallbacks
+ def request_ssl_cert_approval(self, domain, details):
+ """Request the user for ssl approval."""
+ if self._was_ssl_accepted(details):
+ defer.returnValue(True)
+
+ return_code = yield self._launch_ssl_dialog(domain, details)
+ defer.returnValue(return_code == USER_SUCCESS)
=== modified file 'ubuntu_sso/utils/webclient/gsettings.py'
--- ubuntu_sso/utils/webclient/gsettings.py 2012-02-10 14:46:16 +0000
+++ ubuntu_sso/utils/webclient/gsettings.py 2012-03-20 16:10:09 +0000
@@ -31,6 +31,24 @@
return hostname, username, password
+def parse_manual_proxy_settings(scheme, gsettings):
+ """Parse the settings for a given scheme."""
+ host, username, pwd = parse_proxy_host(gsettings[scheme + ".host"])
+ settings = {
+ "host": host,
+ "port": gsettings[scheme + ".port"],
+ }
+ if scheme == "http" and gsettings["http.use-authentication"]:
+ username = gsettings["http.authentication-user"]
+ pwd = gsettings["http.authentication-password"]
+ if username is not None and pwd is not None:
+ settings.update({
+ "username": username,
+ "password": pwd,
+ })
+ return settings
+
+
def get_proxy_settings():
"""Parse the proxy settings as returned by the gsettings executable."""
output = subprocess.check_output(GSETTINGS_CMDLINE.split())
@@ -56,20 +74,9 @@
if mode == "none":
settings = {}
elif mode == "manual":
- # attempt to parse the host
- host, username, pwd = parse_proxy_host(gsettings["http.host"])
- settings = {
- "host": host,
- "port": gsettings["http.port"],
- }
- if gsettings["http.use-authentication"]:
- username = gsettings["http.authentication-user"]
- pwd = gsettings["http.authentication-password"]
- if username is not None and pwd is not None:
- settings.update({
- "username": username,
- "password": pwd,
- })
+ settings = {}
+ for scheme in ["http", "https"]:
+ settings[scheme] = parse_manual_proxy_settings(scheme, gsettings)
else:
# If mode is automatic the PAC javascript should be interpreted
# on each request. That is out of scope so it's ignored for now
=== modified file 'ubuntu_sso/utils/webclient/libsoup.py'
--- ubuntu_sso/utils/webclient/libsoup.py 2012-02-07 19:36:50 +0000
+++ ubuntu_sso/utils/webclient/libsoup.py 2012-03-20 16:10:09 +0000
@@ -19,10 +19,12 @@
from twisted.internet import defer
+from ubuntu_sso.logger import setup_logging
from ubuntu_sso.utils.webclient.common import (
BaseWebClient,
HeaderDict,
Response,
+ ProxyUnauthorizedError,
UnauthorizedError,
WebClientError,
)
@@ -30,6 +32,8 @@
URI_ANONYMOUS_TEMPLATE = "http://{host}:{port}/"
URI_USERNAME_TEMPLATE = "http://{username}:{password}@{host}:{port}/"
+logger = setup_logging("ubuntu_sso.utils.webclient.libsoup")
+
class WebClient(BaseWebClient):
"""A webclient with a libsoup backend."""
@@ -41,11 +45,12 @@
from gi.repository import Soup, SoupGNOME
self.soup = Soup
self.session = Soup.SessionAsync()
- self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
+ self.session.add_feature(SoupGNOME.ProxyResolverGNOME())
self.session.connect("authenticate", self._on_authenticate)
def _on_message(self, session, message, d):
"""Handle the result of an http message."""
+ logger.debug('_on_message status code is %s', message.status_code)
if message.status_code == httplib.OK:
headers = HeaderDict()
response_headers = message.get_property("response-headers")
@@ -57,14 +62,51 @@
elif message.status_code == httplib.UNAUTHORIZED:
e = UnauthorizedError(message.reason_phrase)
d.errback(e)
+ elif message.status_code == httplib.PROXY_AUTHENTICATION_REQUIRED:
+ e = ProxyUnauthorizedError(message.reason_phrase)
+ d.errback(e)
else:
e = WebClientError(message.reason_phrase)
d.errback(e)
- def _on_authenticate(self, sesion, message, auth, retrying, data=None):
+ @defer.inlineCallbacks
+ def _on_authenticate(self, session, message, auth, retrying, data=None):
"""Handle the "authenticate" signal."""
- if not retrying and self.username and self.password:
- auth.authenticate(self.username, self.password)
+ self.session.pause_message(message)
+ try:
+ logger.debug('_on_authenticate: message status code is %s',
+ message.status_code)
+ if not retrying and self.username and self.password:
+ auth.authenticate(self.username, self.password)
+ if auth.is_for_proxy():
+ logger.debug('_on_authenticate auth is for proxy.')
+ got_creds = yield self.request_proxy_auth_credentials(
+ self.session.props.proxy_uri.host,
+ retrying)
+ if got_creds:
+ logger.debug('Got proxy credentials from user.')
+ auth.authenticate(self.proxy_username, self.proxy_password)
+ finally:
+ self.session.unpause_message(message)
+
+ @defer.inlineCallbacks
+ def _on_proxy_authenticate(self, failure, iri, method="GET",
+ extra_headers=None, oauth_credentials=None, post_content=None):
+ """Deal with wrong settings."""
+ failure.trap(ProxyUnauthorizedError)
+ logger.debug('Proxy settings are wrong.')
+ got_creds = yield self.request_proxy_auth_credentials(
+ self.session.props.proxy_uri.host,
+ True)
+ if got_creds:
+ settings = dict(host=self.session.props.proxy_uri.host,
+ port=self.session.props.proxy_uri.port,
+ username=self.proxy_username,
+ password=self.proxy_password)
+ self.force_use_proxy(settings)
+ response = yield self.request(iri, method, extra_headers,
+ oauth_credentials, post_content)
+ defer.returnValue(response)
@defer.inlineCallbacks
def request(self, iri, method="GET", extra_headers=None,
@@ -92,6 +134,8 @@
message.request_body.append(post_content)
self.session.queue_message(message, self._on_message, d)
+ d.addErrback(self._on_proxy_authenticate, iri, method, extra_headers,
+ oauth_credentials, post_content)
response = yield d
defer.returnValue(response)
=== modified file 'ubuntu_sso/utils/webclient/qtnetwork.py'
--- ubuntu_sso/utils/webclient/qtnetwork.py 2012-02-07 19:36:50 +0000
+++ ubuntu_sso/utils/webclient/qtnetwork.py 2012-03-20 16:10:09 +0000
@@ -30,29 +30,61 @@
QNetworkProxyFactory,
QNetworkReply,
QNetworkRequest,
+ QSslCertificate,
)
from twisted.internet import defer
+from ubuntu_sso.logger import setup_logging
from ubuntu_sso.utils.webclient.common import (
BaseWebClient,
HeaderDict,
+ ProxyUnauthorizedError,
Response,
UnauthorizedError,
WebClientError,
)
from ubuntu_sso.utils.webclient import gsettings
+logger = setup_logging("ubuntu_sso.utils.webclient.qtnetwork")
+
+
+def build_proxy(settings_groups):
+ """Create a QNetworkProxy from these settings."""
+ proxy_groups = [
+ ("socks", QNetworkProxy.Socks5Proxy),
+ ("https", QNetworkProxy.HttpProxy),
+ ("http", QNetworkProxy.HttpProxy),
+ ]
+ for group, proxy_type in proxy_groups:
+ if group not in settings_groups:
+ continue
+ settings = settings_groups[group]
+ if "host" in settings and "port" in settings:
+ return QNetworkProxy(proxy_type,
+ hostName=settings.get("host", ""),
+ port=settings.get("port", 0),
+ user=settings.get("username", ""),
+ password=settings.get("password", ""))
+ logger.error("No proxy correctly configured.")
+ return QNetworkProxy(QNetworkProxy.DefaultProxy)
+
class WebClient(BaseWebClient):
"""A webclient with a qtnetwork backend."""
+ proxy_instance = None
+
def __init__(self, *args, **kwargs):
"""Initialize this instance."""
super(WebClient, self).__init__(*args, **kwargs)
self.nam = QNetworkAccessManager(QCoreApplication.instance())
self.nam.finished.connect(self._handle_finished)
self.nam.authenticationRequired.connect(self._handle_authentication)
+ self.nam.proxyAuthenticationRequired.connect(self.handle_proxy_auth)
+ # Disabled until we make this a per-instance option
+ #self.nam.sslErrors.connect(self._handle_ssl_errors)
self.replies = {}
+ self.proxy_retry = False
self.setup_proxy()
def setup_proxy(self):
@@ -60,11 +92,43 @@
# QtNetwork knows how to use the system settings on both Win and Mac
if sys.platform.startswith("linux"):
settings = gsettings.get_proxy_settings()
- if settings:
- self.force_use_proxy(settings)
+ enabled = len(settings) > 0
+ if enabled and WebClient.proxy_instance is None:
+ proxy = build_proxy(settings)
+ QNetworkProxy.setApplicationProxy(proxy)
+ WebClient.proxy_instance = proxy
+ elif enabled and WebClient.proxy_instance:
+ logger.info("Proxy already in use.")
+ else:
+ logger.info("Proxy is disabled.")
else:
QNetworkProxyFactory.setUseSystemConfiguration(True)
+ def handle_proxy_auth(self, proxy, authenticator):
+ """Proxy authentication is required."""
+ logger.info("auth_required %r, %r", self.proxy_username,
+ proxy.hostName())
+ if (self.proxy_username is not None and
+ self.proxy_username != str(authenticator.user())):
+ authenticator.setUser(self.proxy_username)
+ WebClient.proxy_instance.setUser(self.proxy_username)
+ if (self.proxy_password is not None and
+ self.proxy_password != str(authenticator.password())):
+ authenticator.setPassword(self.proxy_password)
+ WebClient.proxy_instance.setPassword(self.proxy_password)
+
+ def _perform_request(self, request, method, post_buffer):
+ """Return a deferred that will be fired with a Response object."""
+ d = defer.Deferred()
+ if method == "GET":
+ reply = self.nam.get(request)
+ elif method == "HEAD":
+ reply = self.nam.head(request)
+ else:
+ reply = self.nam.sendCustomRequest(request, method, post_buffer)
+ self.replies[reply] = d
+ return d
+
@defer.inlineCallbacks
def request(self, iri, method="GET", extra_headers=None,
oauth_credentials=None, post_content=None):
@@ -86,23 +150,30 @@
for key, value in headers.iteritems():
request.setRawHeader(key, value)
- d = defer.Deferred()
- if method == "GET":
- reply = self.nam.get(request)
- elif method == "HEAD":
- reply = self.nam.head(request)
- else:
- post_buffer = QBuffer()
- post_buffer.setData(post_content)
- reply = self.nam.sendCustomRequest(request, method, post_buffer)
- self.replies[reply] = d
- result = yield d
+ post_buffer = QBuffer()
+ post_buffer.setData(post_content)
+ try:
+ result = yield self._perform_request(request, method, post_buffer)
+ except ProxyUnauthorizedError, e:
+ app_proxy = QNetworkProxy.applicationProxy()
+ proxy_host = app_proxy.hostName() if app_proxy else "proxy server"
+ got_creds = yield self.request_proxy_auth_credentials(
+ proxy_host, self.proxy_retry)
+ if got_creds:
+ self.proxy_retry = True
+ result = yield self.request(iri, method, extra_headers,
+ oauth_credentials, post_content)
+ else:
+ excp = WebClientError('Proxy creds needed.', e)
+ defer.returnValue(excp)
defer.returnValue(result)
def _handle_authentication(self, reply, authenticator):
"""The reply needs authentication."""
- authenticator.setUser(self.username)
- authenticator.setPassword(self.password)
+ if authenticator.user() != self.username:
+ authenticator.setUser(self.username)
+ if authenticator.password() != self.password:
+ authenticator.setPassword(self.password)
def _handle_finished(self, reply):
"""The reply has finished processing."""
@@ -118,20 +189,51 @@
d.callback(response)
else:
error_string = reply.errorString()
+ logger.debug('_handle_finished error (%s,%s).', error,
+ error_string)
if error == QNetworkReply.AuthenticationRequiredError:
exception = UnauthorizedError(error_string, content)
+ elif error == QNetworkReply.ProxyAuthenticationRequiredError:
+ # we are going thru a proxy and we did not auth
+ exception = ProxyUnauthorizedError(error_string, content)
else:
exception = WebClientError(error_string, content)
d.errback(exception)
- def force_use_proxy(self, settings):
+ def _get_certificate_details(self, cert):
+ """Return an string with the details of the certificate."""
+ detail_titles = {QSslCertificate.Organization: 'organization',
+ QSslCertificate.CommonName: 'common_name',
+ QSslCertificate.LocalityName: 'locality_name',
+ QSslCertificate.OrganizationalUnitName: 'unit',
+ QSslCertificate.CountryName: 'country_name',
+ QSslCertificate.StateOrProvinceName: 'state_name'}
+ details = {}
+ for info, title in detail_titles.iteritems():
+ details[title] = str(cert.issuerInfo(info))
+ return self.format_ssl_details(details)
+
+ def _get_certificate_host(self, cert):
+ """Return the host of the cert."""
+ return str(cert.issuerInfo(QSslCertificate.CommonName))
+
+ @defer.inlineCallbacks
+ def _handle_ssl_errors(self, reply, errors):
+ """Handle the case in which we got an ssl error."""
+ # ask the user if the cer should be trusted
+ cert = errors[0].certificate()
+ trust_cert = yield self.request_ssl_cert_approval(
+ self._get_certificate_host(cert),
+ self._get_certificate_details(cert))
+ if trust_cert:
+ reply.ignoreSslErrors()
+
+ def force_use_proxy(self, https_settings):
"""Setup this webclient to use the given proxy settings."""
- proxy = QNetworkProxy(QNetworkProxy.HttpProxy,
- hostName=settings.get("host", ""),
- port=settings.get("port", 0),
- user=settings.get("username", ""),
- password=settings.get("password", ""))
- self.nam.setProxy(proxy)
+ settings = {"https": https_settings}
+ proxy = build_proxy(settings)
+ QNetworkProxy.setApplicationProxy(proxy)
+ WebClient.proxy_instance = proxy
def shutdown(self):
"""Shut down all pending requests (if possible)."""
=== modified file 'ubuntu_sso/utils/webclient/tests/__init__.py'
--- ubuntu_sso/utils/webclient/tests/__init__.py 2012-02-07 19:36:50 +0000
+++ ubuntu_sso/utils/webclient/tests/__init__.py 2012-03-20 16:10:09 +0000
@@ -17,9 +17,48 @@
"""Tests for the proxy-aware webclient."""
from twisted.application import internet, service
+from twisted.internet import ssl
from twisted.web import http, server
+# Some settings are not used as described in:
+# https://bugzilla.gnome.org/show_bug.cgi?id=648237
+
+TEMPLATE_GSETTINGS_OUTPUT = """\
+org.gnome.system.proxy autoconfig-url '{autoconfig_url}'
+org.gnome.system.proxy ignore-hosts {ignore_hosts:s}
+org.gnome.system.proxy mode '{mode}'
+org.gnome.system.proxy.ftp host '{ftp_host}'
+org.gnome.system.proxy.ftp port {ftp_port}
+org.gnome.system.proxy.http authentication-password '{auth_password}'
+org.gnome.system.proxy.http authentication-user '{auth_user}'
+org.gnome.system.proxy.http host '{http_host}'
+org.gnome.system.proxy.http port {http_port}
+org.gnome.system.proxy.http use-authentication {http_use_auth}
+org.gnome.system.proxy.https host '{https_host}'
+org.gnome.system.proxy.https port {https_port}
+org.gnome.system.proxy.socks host '{socks_host}'
+org.gnome.system.proxy.socks port {socks_port}
+"""
+
+BASE_GSETTINGS_VALUES = {
+ "autoconfig_url": "",
+ "ignore_hosts": ["localhost", "127.0.0.0/8"],
+ "mode": "none",
+ "ftp_host": "",
+ "ftp_port": 0,
+ "auth_password": "",
+ "auth_user": "",
+ "http_host": "",
+ "http_port": 0,
+ "http_use_auth": "false",
+ "https_host": "",
+ "https_port": 0,
+ "socks_host": "",
+ "socks_port": 0,
+}
+
+
class SaveHTTPChannel(http.HTTPChannel):
"""A save protocol to be used in tests."""
@@ -48,15 +87,26 @@
class BaseMockWebServer(object):
"""A mock webserver for testing"""
- def __init__(self):
+ def __init__(self, ssl_settings=None):
"""Start up this instance."""
self.root = self.get_root_resource()
self.site = SaveSite(self.root)
application = service.Application('web')
self.service_collection = service.IServiceCollection(application)
#pylint: disable=E1101
- self.tcpserver = internet.TCPServer(0, self.site)
- self.tcpserver.setServiceParent(self.service_collection)
+ ssl_context = None
+ if (ssl_settings is not None
+ and 'key' in ssl_settings
+ and 'cert' in ssl_settings):
+ ssl_context = ssl.DefaultOpenSSLContextFactory(ssl_settings['key'],
+ ssl_settings['cert'])
+ self.ssl_server = internet.SSLServer(0, self.site, ssl_context)
+ else:
+ self.ssl_server = None
+ self.server = internet.TCPServer(0, self.site)
+ self.server.setServiceParent(self.service_collection)
+ if self.ssl_server:
+ self.ssl_server.setServiceParent(self.service_collection)
self.service_collection.startService()
def get_root_resource(self):
@@ -65,9 +115,27 @@
def get_iri(self):
"""Build the iri for this mock server."""
- #pylint: disable=W0212
- port_num = self.tcpserver._port.getHost().port
- return u"http://127.0.0.1:%d/" % port_num
+ url = u"http://127.0.0.1:%d/"
+ return url % self.get_port()
+
+ def get_ssl_iri(self):
+ """Build the ssl iri for this mock server."""
+ if self.ssl_server:
+ url = u"https://127.0.0.1:%d/"
+ return url % self.get_ssl_port()
+
+ def get_port(self):
+ """Return the port where we are listening."""
+ # pylint: disable=W0212
+ return self.server._port.getHost().port
+ # pylint: enable=W0212
+
+ def get_ssl_port(self):
+ """Return the ssl port where we are listening."""
+ # pylint: disable=W0212
+ if self.ssl_server:
+ return self.ssl_server._port.getHost().port
+ # pylint: enable=W0212
def stop(self):
"""Shut it down."""
=== modified file 'ubuntu_sso/utils/webclient/tests/test_gsettings.py'
--- ubuntu_sso/utils/webclient/tests/test_gsettings.py 2012-02-10 19:20:33 +0000
+++ ubuntu_sso/utils/webclient/tests/test_gsettings.py 2012-03-20 16:10:09 +0000
@@ -18,43 +18,10 @@
from twisted.trial.unittest import TestCase
from ubuntu_sso.utils.webclient import gsettings
-
-# Some settings are not used as described in:
-# https://bugzilla.gnome.org/show_bug.cgi?id=648237
-
-TEMPLATE_GSETTINGS_OUTPUT = """\
-org.gnome.system.proxy autoconfig-url '{autoconfig_url}'
-org.gnome.system.proxy ignore-hosts {ignore_hosts:s}
-org.gnome.system.proxy mode '{mode}'
-org.gnome.system.proxy.ftp host '{ftp_host}'
-org.gnome.system.proxy.ftp port {ftp_port}
-org.gnome.system.proxy.http authentication-password '{auth_password}'
-org.gnome.system.proxy.http authentication-user '{auth_user}'
-org.gnome.system.proxy.http host '{http_host}'
-org.gnome.system.proxy.http port {http_port}
-org.gnome.system.proxy.http use-authentication {http_use_auth}
-org.gnome.system.proxy.https host '{https_host}'
-org.gnome.system.proxy.https port {https_port}
-org.gnome.system.proxy.socks host '{socks_host}'
-org.gnome.system.proxy.socks port {socks_port}
-"""
-
-BASE_GSETTINGS_VALUES = {
- "autoconfig_url": "",
- "ignore_hosts": ["localhost", "127.0.0.0/8"],
- "mode": "none",
- "ftp_host": "",
- "ftp_port": 0,
- "auth_password": "",
- "auth_user": "",
- "http_host": "",
- "http_port": 0,
- "http_use_auth": "false",
- "https_host": "",
- "https_port": 0,
- "socks_host": "",
- "socks_port": 0,
-}
+from ubuntu_sso.utils.webclient.tests import (
+ BASE_GSETTINGS_VALUES,
+ TEMPLATE_GSETTINGS_OUTPUT,
+)
class ProxySettingsTestCase(TestCase):
@@ -83,25 +50,33 @@
ps = gsettings.get_proxy_settings()
self.assertEqual(ps, expected)
+ def _assert_parser_anonymous(self, scheme):
+ """Assert the parsing of anonymous settings."""
+ template_values = dict(BASE_GSETTINGS_VALUES)
+ expected_host = "expected_host"
+ expected_port = 54321
+ expected = {
+ "host": expected_host,
+ "port": expected_port,
+ }
+ template_values.update({
+ "mode": "manual",
+ scheme + "_host": expected_host,
+ scheme + "_port": expected_port,
+ })
+ fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values)
+ self.patch(gsettings.subprocess, "check_output",
+ lambda _: fake_output)
+ ps = gsettings.get_proxy_settings()
+ self.assertEqual(ps[scheme], expected)
+
def test_gsettings_parser_http_anonymous(self):
"""Test a parser of gsettings."""
- template_values = dict(BASE_GSETTINGS_VALUES)
- expected_host = "expected_host"
- expected_port = 54321
- expected = {
- "host": expected_host,
- "port": expected_port,
- }
- template_values.update({
- "mode": "manual",
- "http_host": expected_host,
- "http_port": expected_port,
- })
- fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values)
- self.patch(gsettings.subprocess, "check_output",
- lambda _: fake_output)
- ps = gsettings.get_proxy_settings()
- self.assertEqual(ps, expected)
+ self._assert_parser_anonymous('http')
+
+ def test_gsettings_parser_https_anonymus(self):
+ """Test a parser of gsettings."""
+ self._assert_parser_anonymous('https')
def test_gsettings_parser_http_authenticated(self):
"""Test a parser of gsettings."""
@@ -128,9 +103,9 @@
self.patch(gsettings.subprocess, "check_output",
lambda _: fake_output)
ps = gsettings.get_proxy_settings()
- self.assertEqual(ps, expected)
+ self.assertEqual(ps["http"], expected)
- def test_gsettings_parser_authenticated_url(self):
+ def _assert_parser_authenticated_url(self, scheme):
"""Test a parser of gsettings with creds in the url."""
template_values = dict(BASE_GSETTINGS_VALUES)
expected_host = "expected_host"
@@ -147,15 +122,23 @@
}
template_values.update({
"mode": "manual",
- "http_host": composed_url,
- "http_port": expected_port,
+ scheme + "_host": composed_url,
+ scheme + "_port": expected_port,
"http_use_auth": "false",
})
fake_output = TEMPLATE_GSETTINGS_OUTPUT.format(**template_values)
self.patch(gsettings.subprocess, "check_output",
lambda _: fake_output)
ps = gsettings.get_proxy_settings()
- self.assertEqual(ps, expected)
+ self.assertEqual(ps[scheme], expected)
+
+ def test_gsettings_parser_http_authenticated_url(self):
+ """Test a parser of gsettings with creds in the url."""
+ self._assert_parser_authenticated_url('http')
+
+ def test_gsettings_parser_https_authenticated_url(self):
+ """Test a parser of gsettings with creds in the url."""
+ self._assert_parser_authenticated_url('https')
def test_gsettings_auth_over_url(self):
"""Test that the settings are more important that the url."""
@@ -166,7 +149,7 @@
expected_password = "very secret password"
composed_url = '%s:%s@%s' % ('user', 'random',
expected_host)
- expected = {
+ http_expected = {
"host": expected_host,
"port": expected_port,
"username": expected_user,
@@ -184,7 +167,7 @@
self.patch(gsettings.subprocess, "check_output",
lambda _: fake_output)
ps = gsettings.get_proxy_settings()
- self.assertEqual(ps, expected)
+ self.assertEqual(ps["http"], http_expected)
class ParseProxyHostTestCase(TestCase):
=== modified file 'ubuntu_sso/utils/webclient/tests/test_webclient.py'
--- ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-02-13 13:14:18 +0000
+++ ubuntu_sso/utils/webclient/tests/test_webclient.py 2012-03-20 16:10:09 +0000
@@ -16,9 +16,12 @@
"""Integration tests for the proxy-enabled webclient."""
import os
+import shutil
import sys
import urllib
+from OpenSSL import crypto
+from socket import gethostname
from twisted.cred import checkers, portal
from twisted.internet import defer
from twisted.web import guard, http, resource
@@ -27,7 +30,15 @@
from ubuntuone.devtools.testcases import TestCase
from ubuntuone.devtools.testcases.squid import SquidTestCase
+from ubuntu_sso import (
+ keyring,
+ EXCEPTION_RAISED,
+ USER_SUCCESS,
+ USER_CANCELLATION,
+)
from ubuntu_sso.utils import webclient
+from ubuntu_sso.utils.ui import SSL_DETAILS_TEMPLATE
+from ubuntu_sso.utils.webclient import gsettings, txweb
from ubuntu_sso.utils.webclient.common import BaseWebClient, HeaderDict, oauth
from ubuntu_sso.utils.webclient.tests import BaseMockWebServer
@@ -188,9 +199,13 @@
return root
-class FakeReactor(object):
- """A fake reactor object."""
- qApp = "Sample qapp"
+class FakeQApplication(object):
+ """A fake Qt module."""
+
+ @classmethod
+ def instance(cls):
+ """Return the instance."""
+ return cls
class ModuleSelectionTestCase(TestCase):
@@ -201,10 +216,11 @@
self.patch(sys, "modules", {})
self.assertFalse(webclient.is_qt4reactor_installed())
- def test_is_qt4reactor_installed_installed(self):
+ def test_is_qt4reactor_installed_installed_core(self):
"""When the qt4reactor is installed, it returns true."""
- fake_sysmodules = {"twisted.internet.reactor": FakeReactor()}
- self.patch(sys, "modules", fake_sysmodules)
+ from PyQt4 import QtCore
+
+ self.patch(QtCore, 'QCoreApplication', FakeQApplication)
self.assertTrue(webclient.is_qt4reactor_installed())
def assert_module_name(self, module, expected_name):
@@ -230,6 +246,7 @@
"""Test for the webclient."""
timeout = 1
+ webclient_factory = webclient.webclient_factory
@defer.inlineCallbacks
def setUp(self):
@@ -237,7 +254,7 @@
self.ws = MockWebServer()
self.addCleanup(self.ws.stop)
self.base_iri = self.ws.get_iri()
- self.wc = webclient.webclient_factory()
+ self.wc = self.webclient_factory()
self.addCleanup(self.wc.shutdown)
@defer.inlineCallbacks
@@ -306,13 +323,22 @@
@defer.inlineCallbacks
def test_send_basic_auth(self):
"""The basic authentication headers are sent."""
- other_wc = webclient.webclient_factory(username=SAMPLE_USERNAME,
- password=SAMPLE_PASSWORD)
+ other_wc = self.webclient_factory(username=SAMPLE_USERNAME,
+ password=SAMPLE_PASSWORD)
self.addCleanup(other_wc.shutdown)
result = yield other_wc.request(self.base_iri + GUARDED)
self.assertEqual(SAMPLE_RESOURCE, result.content)
@defer.inlineCallbacks
+ def test_send_basic_auth_wrong_credentials(self):
+ """Wrong credentials returns a webclient error."""
+ other_wc = self.webclient_factory(username=SAMPLE_USERNAME,
+ password="wrong password!")
+ self.addCleanup(other_wc.shutdown)
+ yield self.assertFailure(other_wc.request(self.base_iri + GUARDED),
+ webclient.UnauthorizedError)
+
+ @defer.inlineCallbacks
def test_request_is_oauth_signed(self):
"""The request is oauth signed."""
tsc = self.wc.get_timestamp_checker()
@@ -348,6 +374,57 @@
"The type of %r must be bytes" % result.content)
+class FakeSavingReactor(object):
+ """A fake reactor that saves connection attempts."""
+
+ def __init__(self):
+ """Initialize this fake instance."""
+ self.connections = []
+
+ def connectTCP(self, host, port, factory, *args):
+ """Fake the connection."""
+ self.connections.append((host, port, args))
+ factory.response_headers = {}
+ factory.deferred = defer.succeed("response content")
+
+ def connectSSL(self, host, port, factory, *args):
+ """Fake the connection."""
+ self.connections.append((host, port, args))
+ factory.response_headers = {}
+ factory.deferred = defer.succeed("response content")
+
+
+class TxWebClientTestCase(WebClientTestCase):
+ """Test case for txweb."""
+
+ webclient_factory = txweb.WebClient
+
+
+class TxWebClientReactorReplaceableTestCase(TestCase):
+ """In the txweb client the reactor is replaceable."""
+
+ timeout = 3
+ FAKE_HOST = u"fake"
+ FAKE_IRI_TEMPLATE = u"%%s://%s/fake_page" % FAKE_HOST
+
+ @defer.inlineCallbacks
+ def _test_replaceable_reactor(self, iri):
+ """The reactor can be replaced with the tunnel client."""
+ fake_reactor = FakeSavingReactor()
+ wc = txweb.WebClient(fake_reactor)
+ _response = yield wc.request(iri)
+ host, _port, _args = fake_reactor.connections[0]
+ self.assertEqual(host, self.FAKE_HOST)
+
+ def test_replaceable_reactor_http(self):
+ """Test the replaceable reactor with an http iri."""
+ return self._test_replaceable_reactor(self.FAKE_IRI_TEMPLATE % "http")
+
+ def test_replaceable_reactor_https(self):
+ """Test the replaceable reactor with an https iri."""
+ return self._test_replaceable_reactor(self.FAKE_IRI_TEMPLATE % "https")
+
+
class TimestampCheckerTestCase(TestCase):
"""Tests for the timestampchecker classmethod."""
@@ -375,6 +452,8 @@
class BasicProxyTestCase(SquidTestCase):
"""Test that the proxy works at all."""
+ timeout = 3
+
@defer.inlineCallbacks
def setUp(self):
yield super(BasicProxyTestCase, self).setUp()
@@ -404,10 +483,119 @@
result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
self.assert_header_contains(result.headers["Via"], "squid")
+ @defer.inlineCallbacks
+ def test_auth_proxy_is_used_creds_requested(self):
+ """The authenticated proxy is used by the webclient."""
+ settings = self.get_auth_proxy_settings()
+ partial_settings = dict(host=settings['host'], port=settings['port'])
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ self.wc.proxy_username = settings['username']
+ self.wc.proxy_password = settings['password']
+ return defer.succeed(True)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ self.wc.force_use_proxy(partial_settings)
+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
+ self.assert_header_contains(result.headers["Via"], "squid")
+
+ @defer.inlineCallbacks
+ def test_auth_proxy_is_requested_creds_bad_details(self):
+ """Test using wrong credentials with the proxy."""
+ settings = self.get_auth_proxy_settings()
+ wrong_settings = dict(host=settings['host'], port=settings['port'],
+ username=settings['password'],
+ password=settings['username'])
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ self.wc.proxy_username = settings['username']
+ self.wc.proxy_password = settings['password']
+ return defer.succeed(True)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ self.wc.force_use_proxy(wrong_settings)
+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
+ self.assert_header_contains(result.headers["Via"], "squid")
+
+ @defer.inlineCallbacks
+ def test_auth_proxy_is_requested_creds_bad_details_user(self):
+ """Test using no creds and user providing the wrong ones."""
+ settings = self.get_auth_proxy_settings()
+ partial_settings = dict(host=settings['host'], port=settings['port'])
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ if retry:
+ self.wc.proxy_username = settings['username']
+ self.wc.proxy_password = settings['password']
+ else:
+ self.wc.proxy_username = settings['password']
+ self.wc.proxy_password = settings['username']
+ return defer.succeed(True)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ self.wc.force_use_proxy(partial_settings)
+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
+ self.assert_header_contains(result.headers["Via"], "squid")
+
+ @defer.inlineCallbacks
+ def test_auth_proxy_is_requested_creds_bad_details_everywhere(self):
+ """Test when we pass the wrong settings and get the wrong settings."""
+ settings = self.get_auth_proxy_settings()
+ wrong_settings = dict(host=settings['host'], port=settings['port'],
+ username=settings['password'],
+ password=settings['username'])
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ if retry:
+ self.wc.proxy_username = settings['username']
+ self.wc.proxy_password = settings['password']
+ else:
+ self.wc.proxy_username = settings['password']
+ self.wc.proxy_password = settings['username']
+ return defer.succeed(True)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ self.wc.force_use_proxy(wrong_settings)
+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
+ self.assert_header_contains(result.headers["Via"], "squid")
+
+ def test_auth_proxy_is_requested_user_cancels(self):
+ """Test when the user cancels the creds dialog."""
+ settings = self.get_auth_proxy_settings()
+ partial_settings = dict(host=settings['host'], port=settings['port'])
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ return defer.succeed(False)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ self.wc.force_use_proxy(partial_settings)
+ self.failUnlessFailure(self.wc.request(self.base_iri + SIMPLERESOURCE),
+ webclient.WebClientError)
+
if WEBCLIENT_MODULE_NAME.endswith(".txweb"):
reason = "txweb does not support proxies."
test_anonymous_proxy_is_used.skip = reason
- test_authenticated_proxy_is_used.skip = reason
+ test_authenticated_proxy_is_used.kip = reason
+ test_auth_proxy_is_used_creds_requested.skip = reason
+ test_auth_proxy_is_requested_creds_bad_details.skip = reason
+ test_auth_proxy_is_requested_creds_bad_details_user.skip = reason
+ test_auth_proxy_is_requested_creds_bad_details_everywhere.skip = reason
+ test_auth_proxy_is_requested_user_cancels.skip = reason
class HeaderDictTestCase(TestCase):
@@ -562,3 +750,315 @@
"""Test for the oauth signing code using HMAC-SHA1."""
oauth_sign = "HMAC-SHA1"
+
+
+class FakeKeyring(object):
+ """A fake keyring."""
+
+ def __init__(self, creds):
+ """A fake keyring."""
+ self.creds = creds
+
+ def __call__(self):
+ """Fake instance callable."""
+ return self
+
+ def get_credentials(self, domain):
+ """A fake get_credentials."""
+ if isinstance(self.creds, Exception):
+ return defer.fail(self.creds)
+ return defer.succeed(self.creds)
+
+
+class RequestProxyAuthTestCase(TestCase):
+ """Test the spawn of the creds dialog."""
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ """Set the different tests."""
+ yield super(RequestProxyAuthTestCase, self).setUp()
+ self.wc = webclient.webclient_factory()
+ self.addCleanup(self.wc.shutdown)
+ self.domain = 'domain'
+ self.retry = False
+ self.creds = dict(username='username', password='password')
+
+ self.keyring = FakeKeyring(self.creds)
+ self.patch(keyring, 'Keyring', self.keyring)
+
+ self.spawn_return_code = USER_SUCCESS
+
+ def fake_spawn_process(args):
+ """Fake spawning a process."""
+ if isinstance(self.spawn_return_code, Exception):
+ return defer.fail(self.spawn_return_code)
+ return defer.succeed(self.spawn_return_code)
+
+ self.patch(webclient.common, 'spawn_program', fake_spawn_process)
+
+ def test_spawn_error(self):
+ """Test the case when we cannot spawn the process."""
+ self.spawn_return_code = Exception()
+ self.failUnlessFailure(self.wc.request_proxy_auth_credentials(
+ self.domain, True),
+ webclient.WebClientError)
+
+ @defer.inlineCallbacks
+ def test_creds_acquired(self):
+ """Test the case in which we do get the creds."""
+ got_creds = yield self.wc.request_proxy_auth_credentials(self.domain,
+ self.retry)
+ self.assertTrue(got_creds, 'Return true when creds are present.')
+ self.assertEqual(self.wc.proxy_username, self.creds['username'])
+ self.assertEqual(self.wc.proxy_password, self.creds['password'])
+
+ def test_creds_acquired_keyring_error(self):
+ """Test the case in which we cannot access the keyring."""
+ self.keyring.creds = Exception()
+ self.failUnlessFailure(self.wc.request_proxy_auth_credentials(
+ self.domain, self.retry),
+ webclient.WebClientError)
+
+ @defer.inlineCallbacks
+ def test_creds_none(self):
+ """Test the case in which we got None from the keyring."""
+ self.keyring.creds = None
+ got_creds = yield self.wc.request_proxy_auth_credentials(self.domain,
+ self.retry)
+ self.assertFalse(got_creds, 'Return false when creds are not present.')
+
+ def test_user_cancelation(self):
+ """Test the case in which the user cancels."""
+ self.spawn_return_code = USER_CANCELLATION
+ got_creds = yield self.wc.request_proxy_auth_credentials(self.domain,
+ self.retry)
+ self.assertFalse(got_creds, 'Return true when user cancels.')
+
+ def test_exception_error(self):
+ """Test the case in which something bad happened."""
+ self.spawn_return_code = EXCEPTION_RAISED
+ got_creds = yield self.wc.request_proxy_auth_credentials(self.domain,
+ self.retry)
+ self.assertFalse(got_creds, 'Return true when user cancels.')
+
+
+class BaseSSLTestCase(SquidTestCase):
+ """Base test that allows to use ssl connections."""
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ """Set the diff tests."""
+ yield super(BaseSSLTestCase, self).setUp()
+ self.cert_dir = os.path.join(self.tmpdir, 'cert')
+ self.cert_details = dict(organization='Canonical',
+ common_name=gethostname(),
+ locality_name='London',
+ unit='Ubuntu One',
+ country_name='UK',
+ state_name='London',)
+ self.ssl_settings = self._generate_self_signed_certificate(
+ self.cert_dir,
+ self.cert_details)
+ self.addCleanup(self._clean_ssl_certificate_files)
+
+ self.ws = MockWebServer(self.ssl_settings)
+ self.addCleanup(self.ws.stop)
+ self.base_iri = self.ws.get_iri()
+ self.base_ssl_iri = self.ws.get_ssl_iri()
+
+ def _clean_ssl_certificate_files(self):
+ """Remove the certificate files."""
+ if os.path.exists(self.cert_dir):
+ shutil.rmtree(self.cert_dir)
+
+ def _generate_self_signed_certificate(self, cert_dir, cert_details):
+ """Generate the required SSL certificates."""
+ if not os.path.exists(cert_dir):
+ os.makedirs(cert_dir)
+ cert_path = os.path.join(cert_dir, 'cert.crt')
+ key_path = os.path.join(cert_dir, 'cert.key')
+
+ if os.path.exists(cert_path):
+ os.unlink(cert_path)
+ if os.path.exists(key_path):
+ os.unlink(key_path)
+
+ # create a key pair
+ key = crypto.PKey()
+ key.generate_key(crypto.TYPE_RSA, 1024)
+
+ # create a self-signed cert
+ cert = crypto.X509()
+ cert.get_subject().C = cert_details['country_name']
+ cert.get_subject().ST = cert_details['state_name']
+ cert.get_subject().L = cert_details['locality_name']
+ cert.get_subject().O = cert_details['organization']
+ cert.get_subject().OU = cert_details['unit']
+ cert.get_subject().CN = cert_details['common_name']
+ cert.set_serial_number(1000)
+ cert.gmtime_adj_notBefore(0)
+ cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+ cert.set_issuer(cert.get_subject())
+ cert.set_pubkey(key)
+ cert.sign(key, 'sha1')
+
+ with open(cert_path, 'wt') as fd:
+ fd.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
+
+ with open(key_path, 'wt') as fd:
+ fd.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
+
+ return dict(key=key_path, cert=cert_path)
+
+
+class CorrectProxyTestCase(BaseSSLTestCase):
+ """Test the interaction with a SSL enabled proxy."""
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ """Set the tests."""
+ yield super(CorrectProxyTestCase, self).setUp()
+
+ # fake the gsettings to have diff settings for https and http
+ http_settings = self.get_auth_proxy_settings()
+
+ #remember so that we can use them in the creds request
+ proxy_username = http_settings['username']
+ proxy_password = http_settings['password']
+
+ # delete the username and password so that we get a 407 for testing
+ del http_settings['username']
+ del http_settings['password']
+
+ https_settings = self.get_nonauth_proxy_settings()
+
+ proxy_settings = dict(http=http_settings, https=https_settings)
+ self.patch(gsettings, "get_proxy_settings", lambda: proxy_settings)
+
+ self.wc = webclient.webclient_factory()
+ self.addCleanup(self.wc.shutdown)
+
+ self.called = []
+
+ def fake_creds_request(domain, retry):
+ """Fake user interaction."""
+ self.called.append('request_proxy_auth_credentials')
+ self.wc.proxy_username = proxy_username
+ self.wc.proxy_password = proxy_password
+ return defer.succeed(True)
+
+ self.patch(self.wc, 'request_proxy_auth_credentials',
+ fake_creds_request)
+
+ def assert_header_contains(self, headers, expected):
+ """One of the headers matching key must contain a given value."""
+ self.assertTrue(any(expected in value for value in headers))
+
+ @defer.inlineCallbacks
+ def test_https_request(self):
+ """Test using the correct proxy for the ssl request.
+
+ In order to assert that the correct proxy is used we expect not to call
+ the auth dialog since we set the https proxy not to use the auth proxy
+ and to fail because we are reaching a https page with bad self-signed
+ certs.
+ """
+ # we fail due to the fake ssl cert
+ yield self.failUnlessFailure(self.wc.request(
+ self.base_ssl_iri + SIMPLERESOURCE),
+ webclient.WebClientError)
+ # https requests do not use the auth proxy therefore called should be
+ # empty. This asserts that we are using the correct settings for the
+ # request.
+ self.assertEqual([], self.called)
+
+ @defer.inlineCallbacks
+ def test_http_request(self):
+ """Test using the correct proxy for the plain request.
+
+ This tests does the opposite to the https tests. We did set the auth
+ proxy for the http request therefore we expect the proxy dialog to be
+ used and not to get an error since we are not visiting a https with bad
+ self-signed certs.
+ """
+ # we do not fail since we are not going to the https page
+ result = yield self.wc.request(self.base_iri + SIMPLERESOURCE)
+ self.assert_header_contains(result.headers["Via"], "squid")
+ # assert that we did go through the auth proxy
+ self.assertIn('request_proxy_auth_credentials', self.called)
+
+ if WEBCLIENT_MODULE_NAME.endswith(".txweb"):
+ reason = 'Multiple proxy settings is not supported.'
+ test_https_request.skip = reason
+ test_http_request.skip = reason
+
+ if WEBCLIENT_MODULE_NAME.endswith(".libsoup"):
+ reason = 'Hard to test since we need to fully mock gsettings.'
+ test_https_request.skip = reason
+ test_http_request.skip = reason
+
+ if WEBCLIENT_MODULE_NAME.endswith(".qtnetwork"):
+ reason = ('Updating proxy settings is not well support due to bug'
+ ' QTBUG-14850: https://bugreports.qt-project.org/'
+ 'browse/QTBUG-14850')
+ test_https_request.skip = reason
+ test_http_request.skip = reason
+
+
+class SSLTestCase(BaseSSLTestCase):
+ """Test error handling when dealing with ssl."""
+
+ @defer.inlineCallbacks
+ def setUp(self):
+ """Set the diff tests."""
+ yield super(SSLTestCase, self).setUp()
+
+ self.wc = webclient.webclient_factory()
+ self.addCleanup(self.wc.shutdown)
+
+ self.return_code = USER_CANCELLATION
+ self.called = []
+
+ def fake_launch_ssl_dialog(client, domain, details):
+ """Fake the ssl dialog."""
+ self.called.append(('_launch_ssl_dialog', domain, details))
+ return defer.succeed(self.return_code)
+
+ self.patch(BaseWebClient, '_launch_ssl_dialog', fake_launch_ssl_dialog)
+
+ @defer.inlineCallbacks
+ def _assert_ssl_fail_user_accepts(self, proxy_settings=None):
+ """Assert the dialog is shown in an ssl fail."""
+ self.return_code = USER_SUCCESS
+ if proxy_settings:
+ self.wc.force_use_proxy(proxy_settings)
+ yield self.wc.request(self.base_ssl_iri + SIMPLERESOURCE)
+ details = SSL_DETAILS_TEMPLATE % self.cert_details
+ self.assertIn(('_launch_ssl_dialog', gethostname(), details),
+ self.called)
+
+ def test_ssl_fail_dialog_user_accepts(self):
+ """Test showing the dialog and accepting."""
+ self._assert_ssl_fail_user_accepts()
+
+ def test_ssl_fail_dialog_user_accepts_via_proxy(self):
+ """Test showing the dialog and accepting when using a proxy."""
+ self._assert_ssl_fail_user_accepts(self.get_nonauth_proxy_settings())
+
+ def test_ssl_fail_dialog_user_rejects(self):
+ """Test showing the dialog and rejecting."""
+ self.failUnlessFailure(self.wc.request(self.base_iri + SIMPLERESOURCE),
+ webclient.WebClientError)
+
+ def test_format_ssl_details(self):
+ """Assert that details are correctly formatted"""
+ details = SSL_DETAILS_TEMPLATE % self.cert_details
+ self.assertEqual(details,
+ self.wc.format_ssl_details(self.cert_details))
+
+ if (WEBCLIENT_MODULE_NAME.endswith(".txweb") or
+ WEBCLIENT_MODULE_NAME.endswith(".libsoup")):
+ reason = 'SSL support has not yet been implemented.'
+ test_ssl_fail_dialog_user_accepts.skip = reason
+ test_ssl_fail_dialog_user_accepts_via_proxy.skip = reason
+ test_ssl_fail_dialog_user_rejects.skip = reason
=== modified file 'ubuntu_sso/utils/webclient/txweb.py'
--- ubuntu_sso/utils/webclient/txweb.py 2012-02-07 19:36:50 +0000
+++ ubuntu_sso/utils/webclient/txweb.py 2012-03-20 16:10:09 +0000
@@ -16,11 +16,9 @@
"""A webclient backend that uses twisted.web.client."""
import base64
-
-from StringIO import StringIO
-
-from twisted.internet import defer, protocol
-from zope.interface import implements
+import urlparse
+
+from twisted.internet import defer
from ubuntu_sso.utils.webclient.common import (
BaseWebClient,
@@ -31,64 +29,80 @@
)
-class StringProtocol(protocol.Protocol):
- """Hold the stuff received in a StringIO."""
-
- # pylint: disable=C0103
- def __init__(self):
- """Initialize this instance."""
- self.deferred = defer.Deferred()
- self.content = StringIO()
-
- def dataReceived(self, data):
- """Some more blocks received."""
- self.content.write(data)
-
- def connectionLost(self, reason=protocol.connectionDone):
- """No more bytes available."""
- self.deferred.callback(self.content.getvalue())
-
-
-class StringProducer(object):
- """Simple implementation of IBodyProducer."""
-
- # delay import, otherwise a default reactor gets installed
- from twisted.web import iweb
-
- implements(iweb.IBodyProducer)
-
- def __init__(self, body):
- """Initialize this instance with some bytes."""
- self.body = body
- self.length = len(body)
-
- # pylint: disable=C0103
- def startProducing(self, consumer):
- """Start producing to the given IConsumer provider."""
- consumer.write(self.body)
- return defer.succeed(None)
-
- def pauseProducing(self):
- """In our case, do nothing."""
-
- def stopProducing(self):
- """In our case, do nothing."""
+class RawResponse(object):
+ """A raw response from the webcall."""
+
+ def __init__(self, headers, content, code=200, phrase="OK"):
+ """Initialize this response."""
+ self.headers = headers
+ self.content = content
+ self.code = code
+ self.phrase = phrase
class WebClient(BaseWebClient):
"""A simple web client that does not support proxies, yet."""
- # delay import, otherwise a default reactor gets installed
- from twisted.internet import reactor
- from twisted.web import client, http, http_headers
-
- # Undefined variable 'http_headers', 'client', 'reactor', 'http'
- # pylint: disable=E0602
+ def __init__(self, connector=None, context_factory=None, **kwargs):
+ """Initialize this webclient."""
+ super(WebClient, self).__init__(**kwargs)
+
+ if connector is None:
+ from twisted.internet import reactor
+ self.connector = reactor
+ else:
+ self.connector = connector
+
+ if context_factory is None:
+ from twisted.internet import ssl
+ self.context_factory = ssl.ClientContextFactory()
+ else:
+ self.context_factory = context_factory
+
+ @defer.inlineCallbacks
+ def raw_request(self, method, uri, headers, postdata):
+ """Make a raw http request."""
+ # delay import, otherwise a default reactor gets installed
+ from twisted.web import client, error
+
+ parsed_url = urlparse.urlparse(uri)
+
+ # pylint: disable=E1101
+ https = parsed_url.scheme == "https"
+ host = parsed_url.netloc.split(":")[0]
+ # pylint: enable=E1101
+ if parsed_url.port is None:
+ port = 443 if https else 80
+ else:
+ port = parsed_url.port
+
+ factory = client.HTTPClientFactory(uri, method=method,
+ postdata=postdata,
+ headers=headers,
+ followRedirect=False)
+ # pylint: disable=E1103
+ if https:
+ self.connector.connectSSL(host, port, factory,
+ self.context_factory)
+ else:
+ self.connector.connectTCP(host, port, factory)
+ # pylint: enable=E1103
+
+ try:
+ content = yield factory.deferred
+ response = RawResponse(factory.response_headers, content)
+ except error.Error as e:
+ response = RawResponse(factory.response_headers, e.response,
+ int(e.status), e.message)
+ defer.returnValue(response)
@defer.inlineCallbacks
def request(self, iri, method="GET", extra_headers=None,
oauth_credentials=None, post_content=None):
"""Get the page, or fail trying."""
+ # delay import, otherwise a default reactor gets installed
+ from twisted.web import http
+
uri = self.iri_to_uri(iri)
if extra_headers:
@@ -107,40 +121,25 @@
headers["Authorization"] = "Basic " + auth
try:
- request_headers = http_headers.Headers()
- for key, value in headers.items():
- request_headers.addRawHeader(key, value)
- agent = client.Agent(reactor)
- if post_content:
- body_producer = StringProducer(post_content)
- else:
- body_producer = None
- agent_response = yield agent.request(method, uri,
- headers=request_headers,
- bodyProducer=body_producer)
- raw_headers = agent_response.headers.getAllRawHeaders()
- response_headers = HeaderDict(raw_headers)
+ raw_response = yield self.raw_request(method, uri,
+ headers=headers,
+ postdata=post_content)
+ response_headers = HeaderDict(raw_response.headers)
if method.lower() != "head":
- response_content = yield self.get_agent_content(agent_response)
+ response_content = raw_response.content
else:
response_content = ""
- if agent_response.code == http.OK:
+ if raw_response.code == http.OK:
defer.returnValue(Response(response_content, response_headers))
- if agent_response.code == http.UNAUTHORIZED:
- raise UnauthorizedError(agent_response.phrase,
+ if raw_response.code == http.UNAUTHORIZED:
+ raise UnauthorizedError(raw_response.phrase,
response_content)
- raise WebClientError(agent_response.phrase, response_content)
+ raise WebClientError(raw_response.phrase, response_content)
except WebClientError:
raise
except Exception as e:
raise WebClientError(e.message, e)
- def get_agent_content(self, agent_response):
- """Get the contents of an agent response."""
- string_protocol = StringProtocol()
- agent_response.deliverBody(string_protocol)
- return string_protocol.deferred
-
def force_use_proxy(self, settings):
"""Setup this webclient to use the given proxy settings."""
- # No proxy support in twisted.web.client
+ # No direct proxy support in twisted.web.client
=== added file 'ubuntu_sso/utils/windows.py'
--- ubuntu_sso/utils/windows.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/windows.py 2012-03-20 16:10:09 +0000
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2010-2012 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, 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, see .
+
+"""Platform specific constants and functions (for Windows)."""
+
+PLATFORM_QSS = ":/windows.qss"