14
15
# You should have received a copy of the GNU General Public License along
15
16
# with this program. If not, see <http://www.gnu.org/licenses/>.
17
# In addition, as a special exception, the copyright holders give
18
# permission to link the code of portions of this program with the
19
# OpenSSL library under certain conditions as described in each
20
# individual source file, and distribute linked combinations
22
# You must obey the GNU General Public License in all respects
23
# for all of the code used other than OpenSSL. If you modify
24
# file(s) with this exception, you may extend this exception to your
25
# version of the file(s), but you are not obligated to do so. If you
26
# do not wish to do so, delete this exception statement from your
27
# version. If you delete this exception statement from all source
28
# files in the program, then also delete it here.
29
17
"""Qt implementation of the UI."""
31
from functools import wraps
33
# pylint: disable=F0401,E0611
34
from PyQt4.QtCore import Qt, pyqtSignal
21
from PyQt4.QtCore import pyqtSignal, Qt, SIGNAL
35
22
from PyQt4.QtGui import (
45
from twisted.internet import defer
47
from ubuntu_sso import main
48
from ubuntu_sso.logger import setup_gui_logging, log_call
49
from ubuntu_sso.qt import (
55
from ubuntu_sso.utils.ui import GENERIC_BACKEND_ERROR
58
logger = setup_gui_logging('ubuntu_sso.sso_wizard_page')
61
class WizardHeader(QFrame):
62
"""WizardHeader Class for Title and Subtitle in all wizard pages."""
64
def __init__(self, max_width, parent=None):
34
from ubuntu_sso.logger import setup_logging
35
from ubuntu_sso.utils.ui import (
39
from ubuntu_sso.qt import common
40
# pylint: disable=F0401,E0611
41
from ubuntu_sso.qt.choose_sign_in_ui import Ui_ChooseSignInPage
42
from ubuntu_sso.qt.current_user_sign_in_ui import Ui_CurrentUserSignInPage
43
from ubuntu_sso.qt.email_verification_ui import Ui_EmailVerificationPage
44
from ubuntu_sso.qt.error_message_ui import Ui_ErrorPage
45
from ubuntu_sso.qt.setup_account_ui import Ui_SetUpAccountPage
46
from ubuntu_sso.qt.success_message_ui import Ui_SuccessPage
47
from ubuntu_sso.qt.forgotten_password_ui import Ui_ForgottenPasswordPage
48
from ubuntu_sso.qt.reset_password_ui import Ui_ResetPasswordPage
49
# pylint: enable=F0401,E0611
50
from ubuntu_sso.qt.controllers import (
51
ChooseSignInController,
52
CurrentUserController,
53
EmailVerificationController,
55
ForgottenPasswordController,
56
ResetPasswordController,
57
SetUpAccountController,
59
UbuntuSSOWizardController)
63
logger = setup_logging('ubuntu_sso.gui')
64
RESET_TITLE = _("Reset password")
65
RESET_SUBTITLE = _("A password reset code has been sent to your e-mail."
66
"Please enter the code below along with your new password.")
69
class Header(QWidget):
70
"""Header Class for Title and Subtitle in all wizard pages."""
65
73
"""Create a new instance."""
66
super(WizardHeader, self).__init__(parent=parent)
67
self.max_width = max_width
68
self.max_title_width = self.max_width * 0.95
69
self.max_subtitle_width = self.max_width * 1.8
74
QWidget.__init__(self)
71
75
vbox = QVBoxLayout(self)
72
vbox.setContentsMargins(0, 0, 0, 0)
76
vbox.setContentsMargins(0, 0, 0, 10)
73
77
self.title_label = QLabel()
74
78
self.title_label.setWordWrap(True)
75
79
self.title_label.setObjectName('title_label')
76
80
self.subtitle_label = QLabel()
77
81
self.subtitle_label.setWordWrap(True)
78
self.subtitle_label.setFixedHeight(32)
79
82
vbox.addWidget(self.title_label)
80
83
vbox.addWidget(self.subtitle_label)
81
84
self.title_label.setVisible(False)
93
95
def set_subtitle(self, subtitle):
94
96
"""Set the Subtitle of the page or hide it otherwise"""
96
maybe_elide_text(self.subtitle_label, subtitle,
97
self.max_subtitle_width)
98
self.subtitle_label.setText(subtitle)
98
99
self.subtitle_label.setVisible(True)
100
101
self.subtitle_label.setVisible(False)
103
class BaseWizardPage(QWizardPage):
104
"""Base class for all wizard pages."""
108
processingStarted = pyqtSignal()
109
processingFinished = pyqtSignal()
111
def __init__(self, parent=None):
112
super(BaseWizardPage, self).__init__(parent=parent)
115
if self.ui_class is not None:
116
# self.ui_class is not callable, pylint: disable=E1102
117
self.ui = self.ui_class()
118
self.ui.setupUi(self)
120
if self.layout() is None:
121
self.setLayout(QVBoxLayout(self))
124
self.form_errors_label = QLabel()
125
self.form_errors_label.setObjectName('form_errors')
126
self.form_errors_label.setAlignment(Qt.AlignBottom)
127
self.layout().insertWidget(0, self.form_errors_label)
130
self.header = WizardHeader(max_width=self.max_width)
131
self.header.set_title(title='')
132
self.header.set_subtitle(subtitle='')
104
class SSOWizardPage(QWizardPage):
105
"""Root class for all wizard pages."""
107
def __init__(self, ui, controller, parent=None):
108
"""Create a new instance."""
109
QWizardPage.__init__(self, parent)
111
self.ui.setupUi(self)
112
self.header = Header()
133
113
self.layout().insertWidget(0, self.header)
135
self.layout().setAlignment(Qt.AlignLeft)
137
self._is_processing = False
139
def _get_is_processing(self):
140
"""Is this widget processing any request?"""
141
return self._is_processing
143
def _set_is_processing(self, new_value):
144
"""Set this widget to be processing a request."""
145
self._is_processing = new_value
146
self.setEnabled(not new_value)
147
if not self._is_processing:
148
self.processingFinished.emit()
150
self.processingStarted.emit()
152
is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
154
# pylint: disable=C0103
156
def cleanupPage(self):
157
"""Hide the errors."""
114
self.controller = controller
116
self.controller.setupUi(self)
119
# pylint: disable=C0103
121
"""Provide the next id."""
123
# pylint: enable=C0103
125
# pylint: disable=C0103
126
def initializePage(self):
127
"""Called to prepare the page just before it is shown."""
129
self.controller.pageInitialized()
130
# pylint: enable=C0103
132
# pylint: disable=C0103
160
133
def setTitle(self, title=''):
161
134
"""Set the Wizard Page Title."""
162
135
self.header.set_title(title)
136
# pylint: enable=C0103
138
# pylint: disable=C0103
164
139
def setSubTitle(self, subtitle=''):
165
140
"""Set the Wizard Page Subtitle."""
166
141
self.header.set_subtitle(subtitle)
169
"""Return the header's title."""
170
return self.header.title_label.text()
173
"""Return the header's subtitle."""
174
return self.header.subtitle_label.text()
176
142
# pylint: enable=C0103
178
@log_call(logger.error)
179
def show_error(self, message):
180
"""Show an error message inside the page."""
181
self.is_processing = False
182
maybe_elide_text(self.form_errors_label, message,
183
self.max_width * 0.95, markup=ERROR_STYLE)
185
def hide_error(self):
186
"""Hide the label errors in the current page."""
187
# We actually want the label with one empty char, because if it is an
188
# empty string, the height of the label is 0
189
self.form_errors_label.setText(' ')
192
class SSOWizardPage(BaseWizardPage):
193
"""Root class for all SSO specific wizard pages."""
195
_signals = {} # override in children
196
max_width = PREFERED_UI_SIZE['width']
198
def __init__(self, app_name, **kwargs):
199
"""Create a new instance."""
200
parent = kwargs.pop('parent', None)
201
super(SSOWizardPage, self).__init__(parent=parent)
203
# store common useful data provided by the app
204
self.app_name = app_name
205
self.ping_url = kwargs.get('ping_url', '')
206
self.tc_url = kwargs.get('tc_url', '')
207
self.policy_url = kwargs.get('policy_url', '')
208
self.help_text = kwargs.get('help_text', '')
210
self._signals_receivers = {}
215
def hide_overlay(self):
216
"""Emit the signal to notify the upper container that ends loading."""
217
self.is_processing = False
219
def show_overlay(self):
220
"""Emit the signal to notify the upper container that is loading."""
221
self.is_processing = True
223
@defer.inlineCallbacks
224
def setup_page(self):
225
"""Setup the widget components."""
226
logger.info('Starting setup_page for: %r', self)
227
# pylint: disable=W0702,W0703
230
client = yield main.get_sso_client()
231
self.backend = client.sso_login
232
self._set_translated_strings()
234
# Call _setup_signals at the end, so we ensure that the UI
235
# is at least styled as expected if the operations with the
237
self._setup_signals()
239
message = 'There was a problem trying to setup the page %r' % self
240
self.show_error(message)
241
logger.exception(message)
242
self.setEnabled(False)
243
# pylint: enable=W0702,W0703
244
logger.info('%r - setup_page ends, backend is %r.', self, self.backend)
246
def _filter_by_app_name(self, f):
247
"""Excecute the decorated function only for 'self.app_name'."""
250
def inner(app_name, *args, **kwargs):
251
"""Execute 'f' only if 'app_name' matches 'self.app_name'."""
253
if app_name == self.app_name:
254
result = f(app_name, *args, **kwargs)
256
logger.info('%s: ignoring call since received app_name '\
257
'"%s" (expected "%s")',
258
f.__name__, app_name, self.app_name)
263
def _setup_signals(self):
264
"""Bind signals to callbacks to be able to test the pages."""
265
for signal, method in self._signals.iteritems():
266
actual = self._signals_receivers.get(signal)
267
if actual is not None:
268
msg = 'Signal %r is already connected with %r.'
269
logger.warning(msg, signal, actual)
271
match = self.backend.connect_to_signal(signal, method)
272
self._signals_receivers[signal] = match
274
def _set_translated_strings(self):
275
"""Implement in each child."""
277
def _connect_ui(self):
278
"""Implement in each child."""
280
def _handle_error(self, remote_call, handler, error):
281
"""Handle any error when calling the remote backend."""
282
logger.error('Remote call %r failed with: %r', remote_call, error)
283
errordict = {'message': GENERIC_BACKEND_ERROR}
284
handler(self.app_name, errordict)
287
145
class EnhancedLineEdit(object):
288
146
"""Represents and enhanced lineedit.
344
197
# create a new enhanced edit
345
198
enhanced_edit = EnhancedLineEdit(edit, cb)
346
199
self._enhanced_edits[edit] = enhanced_edit
202
class ChooseSignInPage(SSOWizardPage):
203
"""Widget that allows the user to choose how to sign in."""
205
def __init__(self, ui, controller, parent=None):
206
"""Create a new widget to be used."""
207
SSOWizardPage.__init__(self, ui, controller, parent)
210
class CurrentUserSignInPage(SSOWizardPage):
211
"""Widget that allows to get the data of user to sign in."""
213
def __init__(self, ui, controller, parent=None):
214
"""Create a new widget to be used."""
215
SSOWizardPage.__init__(self, ui, controller, parent)
218
class EmailVerificationPage(SSOWizardPage):
219
"""Widget used to input the email verification code."""
221
def __init__(self, ui, controller, parent=None):
222
"""Create a new widget to be used."""
223
SSOWizardPage.__init__(self, ui, controller, parent)
226
def verification_code(self):
227
"""Return the content of the verification code edit."""
228
return str(self.ui.verification_code_edit.text())
231
def next_button(self):
232
"""Return the button that move to the next stage."""
233
return self.ui.next_button
236
class ErrorPage(SSOWizardPage):
237
"""Widget used to show the diff errors."""
239
def __init__(self, ui, controller, parent=None):
240
"""Create a new widget to be used."""
241
SSOWizardPage.__init__(self, ui, controller, parent)
244
class ForgottenPasswordPage(SSOWizardEnhancedEditPage):
245
"""Widget used to deal with users that forgot the password."""
247
def __init__(self, ui, controller, parent=None):
248
"""Create a new instance."""
249
SSOWizardEnhancedEditPage.__init__(self, ui, controller, parent)
252
def email_widget(self):
253
"""Return the widget used to show the email information."""
254
return self.ui.email_widget
257
def forgotted_password_intro_label(self):
258
"""Return the intro label that lets the user know the issue."""
259
return self.ui.forgotted_password_intro_label
262
def error_label(self):
263
"""Return the label used to show error."""
264
return self.ui.error_label
267
def email_address_label(self):
268
"""Return the lable used to state the use of the line edit."""
269
return self.ui.email_address_label
272
def email_address(self):
273
"""Return the email address provided by the user."""
274
return str(self.ui.email_line_edit.text())
277
def email_address_line_edit(self):
278
"""Return the line edit with the content."""
279
return self.ui.email_line_edit
282
def send_button(self):
283
"""Return the button used to request the new password."""
284
return self.ui.send_button
287
def try_again_widget(self):
288
"""Return the widget used to display the try again button."""
289
return self.ui.try_again_widget
292
def try_again_button(self):
293
"""Return the button used to try again the reset password."""
294
return self.ui.try_again_button
297
class ResetPasswordPage(SSOWizardEnhancedEditPage):
298
"""Widget used to allow the user change his password."""
300
def __init__(self, ui, controller, parent=None):
301
"""Create a new instance."""
302
SSOWizardEnhancedEditPage.__init__(self, ui, controller, parent)
303
self.ui.password_line_edit.textEdited.connect(
304
lambda: common.password_assistance(self.ui.password_line_edit,
305
self.ui.password_assistance,
307
self.ui.confirm_password_line_edit.textEdited.connect(
308
lambda: common.password_check_match(self.ui.password_line_edit,
309
self.ui.confirm_password_line_edit,
310
self.ui.password_assistance))
312
def focus_changed(self, old, now):
313
"""Check who has the focus to activate password popups if necessary."""
314
if now == self.ui.password_line_edit:
315
self.ui.password_assistance.setVisible(True)
316
common.password_default_assistance(self.ui.password_assistance)
317
elif now == self.ui.confirm_password_line_edit:
318
common.password_check_match(self.ui.password_line_edit,
319
self.ui.confirm_password_line_edit,
320
self.ui.password_assistance)
322
# Invalid name "initializePage"
323
# pylint: disable=C0103
325
def initializePage(self):
326
super(ResetPasswordPage, self).initializePage()
327
common.password_default_assistance(self.ui.password_assistance)
328
self.ui.password_assistance.setVisible(False)
329
self.setTitle(RESET_TITLE)
330
self.setSubTitle(RESET_SUBTITLE)
331
self.ui.password_label.setText(PASSWORD1_ENTRY)
332
self.ui.confirm_password_label.setText(PASSWORD2_ENTRY)
333
self.ui.reset_code.setText(RESET_CODE_ENTRY)
335
def showEvent(self, event):
336
"""Connect focusChanged signal from the application."""
337
super(ResetPasswordPage, self).showEvent(event)
338
self.connect(QApplication.instance(),
339
SIGNAL("focusChanged(QWidget*, QWidget*)"),
342
def hideEvent(self, event):
343
"""Disconnect the focusChanged signal when the page change."""
344
super(ResetPasswordPage, self).hideEvent(event)
346
self.disconnect(QApplication.instance(),
347
SIGNAL("focusChanged(QWidget*, QWidget*)"),
352
# pylint: enable=C0103
355
class SetupAccountPage(SSOWizardEnhancedEditPage):
356
"""Widget used to create a new account."""
358
def __init__(self, ui, controller, parent=None):
359
"""Create a new widget to be used."""
360
SSOWizardEnhancedEditPage.__init__(self, ui, controller, parent)
361
self._enhanced_edits = {}
362
# palettes that will be used to set the colors of the password strengh
363
self.captcha_id = None
364
self.captcha_file = None
365
self.ui.captcha_view.setPixmap(QPixmap())
367
def get_captcha_image(self):
368
"""Return the path to the captcha image."""
369
return self.ui.captcha_view.pixmap()
371
def set_captcha_image(self, pixmap_image):
372
"""Set the new image of the captcha."""
373
# lets set the QPixmap for the label
374
self.ui.captcha_view.setPixmap(pixmap_image)
376
captcha_image = property(get_captcha_image, set_captcha_image)
379
class SuccessPage(SSOWizardPage):
380
"""Page used to display success message."""
382
def __init__(self, ui, controller, parent=None):
383
"""Create a new instance."""
384
SSOWizardPage.__init__(self, ui, controller, parent)
387
class UbuntuSSOWizard(QWizard):
388
"""Wizard used to create or use sso."""
390
# definition of the signals raised by the widget
391
recoverableError = pyqtSignal('QString', 'QString')
392
loginSuccess = pyqtSignal('QString', 'QString')
393
registrationSuccess = pyqtSignal('QString', 'QString')
395
def __init__(self, controller, parent=None, app_name='', tos_url='',
397
"""Create a new wizard."""
398
QWizard.__init__(self, parent)
399
# store common useful data provided by the app
400
self.app_name = app_name
401
self.tos_url = tos_url
402
self.help_text = help_text
404
# set the diff pages of the QWizard
405
self.sign_in_controller = ChooseSignInController(title='Sign In')
406
self.sign_in_page = ChooseSignInPage(Ui_ChooseSignInPage(),
407
self.sign_in_controller,
409
self.setup_controller = SetUpAccountController()
410
self.setup_account = SetupAccountPage(Ui_SetUpAccountPage(),
411
self.setup_controller,
413
self.email_verification = EmailVerificationPage(
414
Ui_EmailVerificationPage(),
415
EmailVerificationController())
416
self.current_user_controller = CurrentUserController(title='Sign in')
417
self.current_user = CurrentUserSignInPage(Ui_CurrentUserSignInPage(),
418
self.current_user_controller,
420
self.success_controller = SuccessController()
421
self.success = SuccessPage(Ui_SuccessPage(), self.success_controller,
423
self.error_controller = ErrorController()
424
self.error = ErrorPage(Ui_ErrorPage(), self.error_controller)
425
self.forgotte_pwd_controller = ForgottenPasswordController()
426
self.forgotten = ForgottenPasswordPage(Ui_ForgottenPasswordPage(),
427
self.forgotte_pwd_controller,
429
self.reset_password_controller = ResetPasswordController()
430
self.reset_password = ResetPasswordPage(Ui_ResetPasswordPage(),
431
self.reset_password_controller,
433
# store the ids of the pages so that it is easier to access them later
435
for page in [self.sign_in_page, self.setup_account,
436
self.email_verification, self.current_user, self.success,
437
self.error, self.forgotten, self.reset_password]:
438
self._pages[page] = self.addPage(page)
440
# set the buttons layout to only have cancel and back since the next
441
# buttons are the ones used in the diff pages.
443
buttons_layout.append(QWizard.Stretch)
444
buttons_layout.append(QWizard.BackButton)
445
buttons_layout.append(QWizard.CancelButton)
446
self.setButtonLayout(buttons_layout)
447
self.setWindowTitle(app_name)
448
self.controller = controller
449
self.controller.setupUi(self)
452
def sign_in_page_id(self):
453
"""Return the id of the page used for choosing sign in type."""
454
return self._pages[self.sign_in_page]
457
def setup_account_page_id(self):
458
"""Return the id of the page used for sign in."""
459
return self._pages[self.setup_account]
462
def email_verification_page_id(self):
463
"""Return the id of the verification page."""
464
return self._pages[self.email_verification]
467
def current_user_page_id(self):
468
"""Return the id used to signin by a current user."""
469
return self._pages[self.current_user]
472
def success_page_id(self):
473
"""Return the id of the success page."""
474
return self._pages[self.success]
477
def forgotten_password_page_id(self):
478
"""Return the id of the forgotten password page."""
479
return self._pages[self.forgotten]
482
def reset_password_page_id(self):
483
"""Return the id of the reset password page."""
484
return self._pages[self.reset_password]
487
def error_page_id(self):
488
"""Return the id of the error page."""
489
return self._pages[self.error]
492
class UbuntuSSOClientGUI(object):
493
"""Ubuntu single sign-on GUI."""
495
def __init__(self, app_name, tc_url='http://one.ubuntu.com', help_text='',
496
window_id=0, login_only=False):
497
"""Create a new instance."""
498
# create the controller and the ui, then set the cb and call the show
499
# method so that we can work
500
self.controller = UbuntuSSOWizardController(app_name)
501
self.view = UbuntuSSOWizard(self.controller, app_name=app_name,
502
tos_url=tc_url, help_text=help_text)
505
def get_login_success_callback(self):
506
"""Return the log in success cb."""
507
return self.controller.login_success_callback
509
def set_login_success_callback(self, cb):
510
"""Set log in success cb."""
511
self.controller.login_success_callback = cb
513
login_success_callback = property(get_login_success_callback,
514
set_login_success_callback)
516
def get_registration_success_callback(self):
517
"""Return the registration success cb."""
518
return self.controller.registration_success_callback
520
def set_registration_success_callback(self, cb):
521
"""Set registration success cb."""
522
self.controller.registration_success_callback = cb
524
registration_success_callback = property(get_registration_success_callback,
525
set_registration_success_callback)
527
def get_user_cancellation_callback(self):
528
"""Return the user cancellation callback."""
529
return self.controller.user_cancellation_callback
531
def set_user_cancellation_callback(self, cb):
532
"""Set the user cancellation callback."""
533
self.controller.user_cancellation_callback = cb
535
user_cancellation_callback = property(get_user_cancellation_callback,
536
set_user_cancellation_callback)