~nataliabidart/ubuntu-sso-client/find-me-bin-dir

« back to all changes in this revision

Viewing changes to ubuntu_sso/qt/gui.py

- Refactor the pages and controller in sso (LP: #929686).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- coding: utf-8 -*-
2
2
#
3
 
# Copyright 2011 Canonical Ltd.
 
3
# Copyright 2011-2012 Canonical Ltd.
4
4
#
5
5
# This program is free software: you can redistribute it and/or modify it
6
6
# under the terms of the GNU General Public License version 3, as published
16
16
"""Qt implementation of the UI."""
17
17
 
18
18
import gettext
 
19
from functools import wraps
19
20
 
20
21
# pylint: disable=F0401,E0611
21
22
 
22
 
from PyQt4.QtCore import pyqtSignal, Qt, SIGNAL
 
23
from PyQt4.QtCore import Qt
23
24
from PyQt4.QtGui import (
24
25
    QApplication,
25
26
    QWidget,
26
27
    QCursor,
27
28
    QHBoxLayout,
28
29
    QVBoxLayout,
29
 
    QPixmap,
 
30
    QMessageBox,
30
31
    QStyle,
31
 
    QWizard,
32
32
    QWizardPage,
33
33
    QLabel,
34
34
)
 
35
from twisted.internet import defer
35
36
 
 
37
from ubuntu_sso import main
 
38
from ubuntu_sso.qt.loadingoverlay import LoadingOverlay
36
39
from ubuntu_sso.logger import setup_logging
37
 
from ubuntu_sso.qt import common
38
 
from ubuntu_sso.qt.controllers import (
39
 
    ChooseSignInController,
40
 
    CurrentUserController,
41
 
    EmailVerificationController,
42
 
    ErrorController,
43
 
    ForgottenPasswordController,
44
 
    ResetPasswordController,
45
 
    SetUpAccountController,
46
 
    SuccessController,
47
 
    UbuntuSSOWizardController,
48
 
)
49
 
from ubuntu_sso.qt.ui.choose_sign_in_ui import Ui_ChooseSignInPage
50
 
from ubuntu_sso.qt.ui.current_user_sign_in_ui import Ui_CurrentUserSignInPage
51
 
from ubuntu_sso.qt.ui.email_verification_ui import Ui_EmailVerificationPage
52
 
from ubuntu_sso.qt.ui.error_message_ui import Ui_ErrorPage
53
 
from ubuntu_sso.qt.ui.setup_account_ui import Ui_SetUpAccountPage
54
 
from ubuntu_sso.qt.ui.success_message_ui import Ui_SuccessPage
55
 
from ubuntu_sso.qt.ui.forgotten_password_ui import Ui_ForgottenPasswordPage
56
 
from ubuntu_sso.qt.ui.reset_password_ui import Ui_ResetPasswordPage
57
 
from ubuntu_sso.utils.ui import (
58
 
    PASSWORD1_ENTRY,
59
 
    PASSWORD2_ENTRY,
60
 
    RESET_CODE_ENTRY,
61
 
)
 
40
 
62
41
 
63
42
_ = gettext.gettext
64
43
logger = setup_logging('ubuntu_sso.gui')
65
 
RESET_TITLE = _("Reset password")
66
 
RESET_SUBTITLE = _("A password reset code has been sent to your e-mail."
67
 
                   "Please enter the code below along with your new password.")
68
44
 
69
45
 
70
46
class Header(QWidget):
105
81
class SSOWizardPage(QWizardPage):
106
82
    """Root class for all wizard pages."""
107
83
 
108
 
    def __init__(self, ui, controller, parent=None):
 
84
    def __init__(self, ui, app_name=None, title='', subtitle='', parent=None):
109
85
        """Create a new instance."""
110
86
        super(SSOWizardPage, self).__init__(parent)
111
87
        self.ui = ui
112
88
        self.ui.setupUi(self)
 
89
        self.overlay = LoadingOverlay(self)
 
90
        self.overlay.hide()
 
91
        self.app_name = app_name
113
92
        self.header = Header()
 
93
        self.header.set_title(title)
 
94
        self.header.set_subtitle(subtitle)
114
95
        self.layout().insertWidget(0, self.header)
115
 
        self.controller = controller
116
 
        if self.controller:
117
 
            self.controller.setupUi(self)
 
96
        self.message_box = QMessageBox
118
97
        self.next = -1
 
98
        self._signals = {}
 
99
        self._signals_receivers = {}
 
100
        self.backend = None
 
101
 
 
102
    @defer.inlineCallbacks
 
103
    def get_backend(self):
 
104
        """Return the backend used by the controller."""
 
105
        if self.backend is None:
 
106
            client = yield main.get_sso_client()
 
107
            self.backend = client.sso_login
 
108
        defer.returnValue(self.backend)
119
109
 
120
110
    # pylint: disable=C0103
121
111
    def nextId(self):
122
112
        """Provide the next id."""
123
113
        return self.next
124
 
    # pylint: enable=C0103
125
 
 
126
 
    # pylint: disable=C0103
127
 
    def initializePage(self):
128
 
        """Called to prepare the page just before it is shown."""
129
 
        if self.controller:
130
 
            self.controller.pageInitialized()
131
 
    # pylint: enable=C0103
132
 
 
133
 
    # pylint: disable=C0103
 
114
 
 
115
    def resizeEvent(self, event):
 
116
        """Resize the overlay to fit all the widget."""
 
117
        QWizardPage.resizeEvent(self, event)
 
118
        self.overlay.resize(event.size())
 
119
 
134
120
    def setTitle(self, title=''):
135
121
        """Set the Wizard Page Title."""
136
122
        self.header.set_title(title)
137
 
    # pylint: enable=C0103
138
123
 
139
 
    # pylint: disable=C0103
140
124
    def setSubTitle(self, subtitle=''):
141
125
        """Set the Wizard Page Subtitle."""
142
126
        self.header.set_subtitle(subtitle)
143
127
    # pylint: enable=C0103
144
128
 
 
129
    def _filter_by_app_name(self, f):
 
130
        """Excecute the decorated function only for 'self.app_name'."""
 
131
 
 
132
        @wraps(f)
 
133
        def inner(app_name, *args, **kwargs):
 
134
            """Execute 'f' only if 'app_name' matches 'self.app_name'."""
 
135
            result = None
 
136
            if app_name == self.app_name:
 
137
                result = f(app_name, *args, **kwargs)
 
138
            else:
 
139
                logger.info('%s: ignoring call since received app_name '\
 
140
                            '"%s" (expected "%s")',
 
141
                            f.__name__, app_name, self.app_name)
 
142
            return result
 
143
 
 
144
        return inner
 
145
 
 
146
    def _setup_signals(self):
 
147
        """Bind signals to callbacks to be able to test the pages."""
 
148
        for signal, method in self._signals.iteritems():
 
149
            actual = self._signals_receivers.get(signal)
 
150
            if actual is not None:
 
151
                msg = 'Signal %r is already connected with %r.'
 
152
                logger.warning(msg, signal, actual)
 
153
 
 
154
            match = self.backend.connect_to_signal(signal, method)
 
155
            self._signals_receivers[signal] = match
 
156
 
145
157
 
146
158
class EnhancedLineEdit(object):
147
159
    """Represents and enhanced lineedit.
186
198
class SSOWizardEnhancedEditPage(SSOWizardPage):
187
199
    """Page that contains enhanced line edits."""
188
200
 
189
 
    def __init__(self, ui, controller, parent=None):
 
201
    def __init__(self, ui, app_name=None, parent=None):
190
202
        """Create a new instance."""
191
203
        self._enhanced_edits = {}
192
 
        super(SSOWizardEnhancedEditPage, self).__init__(ui, controller, parent)
 
204
        super(SSOWizardEnhancedEditPage, self).__init__(ui,
 
205
            app_name=app_name, parent=parent)
193
206
 
194
207
    def set_line_edit_validation_rule(self, edit, cb):
195
208
        """Set a new enhanced edit so that we can show an icon."""
199
212
            # create a new enhanced edit
200
213
            enhanced_edit = EnhancedLineEdit(edit, cb)
201
214
            self._enhanced_edits[edit] = enhanced_edit
202
 
 
203
 
 
204
 
class ChooseSignInPage(SSOWizardPage):
205
 
    """Widget that allows the user to choose how to sign in."""
206
 
 
207
 
 
208
 
class CurrentUserSignInPage(SSOWizardPage):
209
 
    """Widget that allows to get the data of user to sign in."""
210
 
 
211
 
 
212
 
class EmailVerificationPage(SSOWizardPage):
213
 
    """Widget used to input the email verification code."""
214
 
 
215
 
    @property
216
 
    def verification_code(self):
217
 
        """Return the content of the verification code edit."""
218
 
        return str(self.ui.verification_code_edit.text())
219
 
 
220
 
    @property
221
 
    def next_button(self):
222
 
        """Return the button that move to the next stage."""
223
 
        return self.ui.next_button
224
 
 
225
 
 
226
 
class ErrorPage(SSOWizardPage):
227
 
    """Widget used to show the diff errors."""
228
 
 
229
 
 
230
 
class ForgottenPasswordPage(SSOWizardEnhancedEditPage):
231
 
    """Widget used to deal with users that forgot the password."""
232
 
 
233
 
    @property
234
 
    def email_widget(self):
235
 
        """Return the widget used to show the email information."""
236
 
        return self.ui.email_widget
237
 
 
238
 
    @property
239
 
    def forgotted_password_intro_label(self):
240
 
        """Return the intro label that lets the user know the issue."""
241
 
        return self.ui.forgotted_password_intro_label
242
 
 
243
 
    @property
244
 
    def error_label(self):
245
 
        """Return the label used to show error."""
246
 
        return self.ui.error_label
247
 
 
248
 
    @property
249
 
    def email_address_label(self):
250
 
        """Return the lable used to state the use of the line edit."""
251
 
        return self.ui.email_address_label
252
 
 
253
 
    @property
254
 
    def email_address(self):
255
 
        """Return the email address provided by the user."""
256
 
        return str(self.ui.email_line_edit.text())
257
 
 
258
 
    @property
259
 
    def email_address_line_edit(self):
260
 
        """Return the line edit with the content."""
261
 
        return self.ui.email_line_edit
262
 
 
263
 
    @property
264
 
    def send_button(self):
265
 
        """Return the button used to request the new password."""
266
 
        return self.ui.send_button
267
 
 
268
 
    @property
269
 
    def try_again_widget(self):
270
 
        """Return the widget used to display the try again button."""
271
 
        return self.ui.try_again_widget
272
 
 
273
 
    @property
274
 
    def try_again_button(self):
275
 
        """Return the button used to try again the reset password."""
276
 
        return self.ui.try_again_button
277
 
 
278
 
 
279
 
class ResetPasswordPage(SSOWizardEnhancedEditPage):
280
 
    """Widget used to allow the user change his password."""
281
 
 
282
 
    def __init__(self, ui, controller, parent=None):
283
 
        """Create a new instance."""
284
 
        super(ResetPasswordPage, self).__init__(ui, controller, parent)
285
 
        self.ui.password_line_edit.textEdited.connect(
286
 
            lambda: common.password_assistance(self.ui.password_line_edit,
287
 
                                                 self.ui.password_assistance,
288
 
                                                 common.NORMAL))
289
 
        self.ui.confirm_password_line_edit.textEdited.connect(
290
 
            lambda: common.password_check_match(self.ui.password_line_edit,
291
 
                                      self.ui.confirm_password_line_edit,
292
 
                                      self.ui.password_assistance))
293
 
 
294
 
    def focus_changed(self, old, now):
295
 
        """Check who has the focus to activate password popups if necessary."""
296
 
        if now == self.ui.password_line_edit:
297
 
            self.ui.password_assistance.setVisible(True)
298
 
            common.password_default_assistance(self.ui.password_assistance)
299
 
        elif now == self.ui.confirm_password_line_edit:
300
 
            common.password_check_match(self.ui.password_line_edit,
301
 
                                      self.ui.confirm_password_line_edit,
302
 
                                      self.ui.password_assistance)
303
 
 
304
 
    # Invalid name "initializePage"
305
 
    # pylint: disable=C0103
306
 
 
307
 
    def initializePage(self):
308
 
        super(ResetPasswordPage, self).initializePage()
309
 
        common.password_default_assistance(self.ui.password_assistance)
310
 
        self.ui.password_assistance.setVisible(False)
311
 
        self.setTitle(RESET_TITLE)
312
 
        self.setSubTitle(RESET_SUBTITLE)
313
 
        self.ui.password_label.setText(PASSWORD1_ENTRY)
314
 
        self.ui.confirm_password_label.setText(PASSWORD2_ENTRY)
315
 
        self.ui.reset_code.setText(RESET_CODE_ENTRY)
316
 
 
317
 
    def showEvent(self, event):
318
 
        """Connect focusChanged signal from the application."""
319
 
        super(ResetPasswordPage, self).showEvent(event)
320
 
        self.connect(QApplication.instance(),
321
 
            SIGNAL("focusChanged(QWidget*, QWidget*)"),
322
 
            self.focus_changed)
323
 
 
324
 
    def hideEvent(self, event):
325
 
        """Disconnect the focusChanged signal when the page change."""
326
 
        super(ResetPasswordPage, self).hideEvent(event)
327
 
        try:
328
 
            self.disconnect(QApplication.instance(),
329
 
                SIGNAL("focusChanged(QWidget*, QWidget*)"),
330
 
                self.focus_changed)
331
 
        except TypeError:
332
 
            pass
333
 
 
334
 
    # pylint: enable=C0103
335
 
 
336
 
 
337
 
class SetupAccountPage(SSOWizardEnhancedEditPage):
338
 
    """Widget used to create a new account."""
339
 
 
340
 
    def __init__(self, ui, controller, parent=None):
341
 
        """Create a new widget to be used."""
342
 
        super(SetupAccountPage, self).__init__(ui, controller, parent)
343
 
        # palettes that will be used to set the colors of the password strengh
344
 
        self.captcha_id = None
345
 
        self.captcha_file = None
346
 
        self.ui.captcha_view.setPixmap(QPixmap())
347
 
 
348
 
    def get_captcha_image(self):
349
 
        """Return the path to the captcha image."""
350
 
        return self.ui.captcha_view.pixmap()
351
 
 
352
 
    def set_captcha_image(self, pixmap_image):
353
 
        """Set the new image of the captcha."""
354
 
        # lets set the QPixmap for the label
355
 
        self.ui.captcha_view.setPixmap(pixmap_image)
356
 
 
357
 
    captcha_image = property(get_captcha_image, set_captcha_image)
358
 
 
359
 
 
360
 
class SuccessPage(SSOWizardPage):
361
 
    """Page used to display success message."""
362
 
 
363
 
 
364
 
class UbuntuSSOWizard(QWizard):
365
 
    """Wizard used to create or use sso."""
366
 
 
367
 
    # definition of the signals raised by the widget
368
 
    recoverableError = pyqtSignal('QString', 'QString')
369
 
    loginSuccess = pyqtSignal('QString', 'QString')
370
 
    registrationSuccess = pyqtSignal('QString', 'QString')
371
 
 
372
 
    def __init__(self, controller, app_name, **kwargs):
373
 
        """Create a new wizard."""
374
 
        parent = kwargs.get('parent')
375
 
        super(UbuntuSSOWizard, self).__init__(parent)
376
 
 
377
 
        # store common useful data provided by the app
378
 
        self.app_name = app_name
379
 
        self.ping_url = kwargs.get('ping_url', '')
380
 
        self.tc_url = kwargs.get('tc_url', '')
381
 
        self.help_text = kwargs.get('help_text', '')
382
 
        self.login_only = kwargs.get('login_only', False)
383
 
        self.close_callback = kwargs.get('close_callback', lambda: None)
384
 
 
385
 
        # set the diff pages of the QWizard
386
 
        self.sign_in_controller = ChooseSignInController(title='Sign In')
387
 
        self.sign_in_page = ChooseSignInPage(Ui_ChooseSignInPage(),
388
 
                                             self.sign_in_controller,
389
 
                                             parent=self)
390
 
        self.setup_controller = SetUpAccountController()
391
 
        self.setup_account = SetupAccountPage(Ui_SetUpAccountPage(),
392
 
                                              self.setup_controller,
393
 
                                              parent=self)
394
 
        self.email_verification = EmailVerificationPage(
395
 
                                                Ui_EmailVerificationPage(),
396
 
                                                EmailVerificationController())
397
 
        self.current_user_controller = CurrentUserController(title='Sign in')
398
 
        self.current_user = CurrentUserSignInPage(Ui_CurrentUserSignInPage(),
399
 
                                                  self.current_user_controller,
400
 
                                                  parent=self)
401
 
        self.success_controller = SuccessController()
402
 
        self.success = SuccessPage(Ui_SuccessPage(), self.success_controller,
403
 
                                   parent=self)
404
 
        self.error_controller = ErrorController()
405
 
        self.error = ErrorPage(Ui_ErrorPage(), self.error_controller)
406
 
        self.forgotte_pwd_controller = ForgottenPasswordController()
407
 
        self.forgotten = ForgottenPasswordPage(Ui_ForgottenPasswordPage(),
408
 
                                               self.forgotte_pwd_controller,
409
 
                                               parent=self)
410
 
        self.reset_password_controller = ResetPasswordController()
411
 
        self.reset_password = ResetPasswordPage(Ui_ResetPasswordPage(),
412
 
                                                self.reset_password_controller,
413
 
                                                parent=self)
414
 
        # store the ids of the pages so that it is easier to access them later
415
 
        self._pages = {}
416
 
        for page in [self.sign_in_page, self.setup_account,
417
 
                     self.email_verification, self.current_user, self.success,
418
 
                     self.error, self.forgotten, self.reset_password]:
419
 
            self._pages[page] = self.addPage(page)
420
 
 
421
 
        # set the buttons layout to only have cancel and back since the next
422
 
        # buttons are the ones used in the diff pages.
423
 
        buttons_layout = []
424
 
        buttons_layout.append(QWizard.Stretch)
425
 
        buttons_layout.append(QWizard.BackButton)
426
 
        buttons_layout.append(QWizard.CancelButton)
427
 
        self.setButtonLayout(buttons_layout)
428
 
        self.setWindowTitle(self.app_name)
429
 
        self.controller = controller
430
 
        self.controller.setupUi(self)
431
 
 
432
 
    @property
433
 
    def sign_in_page_id(self):
434
 
        """Return the id of the page used for choosing sign in type."""
435
 
        return self._pages[self.sign_in_page]
436
 
 
437
 
    @property
438
 
    def setup_account_page_id(self):
439
 
        """Return the id of the page used for sign in."""
440
 
        return self._pages[self.setup_account]
441
 
 
442
 
    @property
443
 
    def email_verification_page_id(self):
444
 
        """Return the id of the verification page."""
445
 
        return self._pages[self.email_verification]
446
 
 
447
 
    @property
448
 
    def current_user_page_id(self):
449
 
        """Return the id used to signin by a current user."""
450
 
        return self._pages[self.current_user]
451
 
 
452
 
    @property
453
 
    def success_page_id(self):
454
 
        """Return the id of the success page."""
455
 
        return self._pages[self.success]
456
 
 
457
 
    @property
458
 
    def forgotten_password_page_id(self):
459
 
        """Return the id of the forgotten password page."""
460
 
        return self._pages[self.forgotten]
461
 
 
462
 
    @property
463
 
    def reset_password_page_id(self):
464
 
        """Return the id of the reset password page."""
465
 
        return self._pages[self.reset_password]
466
 
 
467
 
    @property
468
 
    def error_page_id(self):
469
 
        """Return the id of the error page."""
470
 
        return self._pages[self.error]
471
 
 
472
 
 
473
 
class UbuntuSSOClientGUI(object):
474
 
    """Ubuntu single sign-on GUI."""
475
 
 
476
 
    def __init__(self, app_name, **kwargs):
477
 
        """Create a new instance."""
478
 
        super(UbuntuSSOClientGUI, self).__init__()
479
 
        logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.',
480
 
                     app_name, kwargs)
481
 
        self.app_name = app_name
482
 
        # create the controller and the ui, then set the cb and call the show
483
 
        # method so that we can work
484
 
        self.controller = UbuntuSSOWizardController(app_name)
485
 
        self.view = UbuntuSSOWizard(self.controller, app_name=app_name,
486
 
                                    **kwargs)
487
 
        self.view.show()
488
 
 
489
 
    def get_login_success_callback(self):
490
 
        """Return the log in success cb."""
491
 
        return self.controller.login_success_callback
492
 
 
493
 
    def set_login_success_callback(self, cb):
494
 
        """Set log in success cb."""
495
 
        self.controller.login_success_callback = cb
496
 
 
497
 
    login_success_callback = property(get_login_success_callback,
498
 
                                      set_login_success_callback)
499
 
 
500
 
    def get_registration_success_callback(self):
501
 
        """Return the registration success cb."""
502
 
        return self.controller.registration_success_callback
503
 
 
504
 
    def set_registration_success_callback(self, cb):
505
 
        """Set registration success cb."""
506
 
        self.controller.registration_success_callback = cb
507
 
 
508
 
    registration_success_callback = property(get_registration_success_callback,
509
 
                                             set_registration_success_callback)
510
 
 
511
 
    def get_user_cancellation_callback(self):
512
 
        """Return the user cancellation callback."""
513
 
        return self.controller.user_cancellation_callback
514
 
 
515
 
    def set_user_cancellation_callback(self, cb):
516
 
        """Set the user cancellation callback."""
517
 
        self.controller.user_cancellation_callback = cb
518
 
 
519
 
    user_cancellation_callback = property(get_user_cancellation_callback,
520
 
                                          set_user_cancellation_callback)