~mvo/ubuntu-sso-client/strawman-lp711413

« back to all changes in this revision

Viewing changes to ubuntu_sso/qt/controllers.py

  • Committer: Tarmac
  • Author(s): Manuel de la Pena, ralsina, Roberto Alsina
  • Date: 2011-04-14 17:01:56 UTC
  • mfrom: (705.2.2 forgotten_password)
  • Revision ID: tarmac-20110414170156-4k8s1708gzgj5oi2
Fixes lp:753281

Adds the required UI and backend to allow a windows user to reset his sso password from the Windows client. Tests have been added to ensure that the backend is correctly called.

Show diffs side-by-side

added added

removed removed

Lines of Context:
32
32
    CAPTCHA_SOLUTION_ENTRY,
33
33
    EMAIL1_ENTRY,
34
34
    EMAIL2_ENTRY,
 
35
    EMAIL_LABEL,
35
36
    EMAIL_MISMATCH,
36
37
    EMAIL_INVALID,
37
38
    ERROR,
44
45
    PASSWORD_HELP,
45
46
    PASSWORD_MISMATCH,
46
47
    PASSWORD_TOO_WEAK,
 
48
    REQUEST_PASSWORD_TOKEN_LABEL,
 
49
    RESET_PASSWORD,
 
50
    RESET_CODE_ENTRY,
 
51
    REQUEST_PASSWORD_TOKEN_WRONG_EMAIL,
 
52
    REQUEST_PASSWORD_TOKEN_TECH_ERROR,
47
53
    SET_UP_ACCOUNT_CHOICE_BUTTON,
48
54
    SET_UP_ACCOUNT_BUTTON,
49
55
    SIGN_IN_BUTTON,
50
56
    SURNAME_ENTRY,
51
57
    SUCCESS,
52
58
    TC_BUTTON,
 
59
    TRY_AGAIN_BUTTON,
53
60
    VERIFY_EMAIL_TITLE,
54
61
    VERIFY_EMAIL_CONTENT,
55
62
    YES_TO_TC,
56
63
    get_password_strength,
57
 
    is_min_required_password)
 
64
    is_min_required_password,
 
65
    is_correct_email)
58
66
 
59
67
 
60
68
logger = setup_logging('ubuntu_sso.controllers')
 
69
FAKE_URL = '<a href="http://one.ubuntu.com">%s</a>'
 
70
 
61
71
# pylint: disable=W0511
62
72
# disabled warnings about TODO comments
63
73
 
97
107
class ChooseSignInController(object):
98
108
    """Controlled to the ChooseSignIn view/widget."""
99
109
 
100
 
    def __init__(self, title='', existing_account_id=1,
101
 
                 new_account_id=2):
 
110
    def __init__(self, title=''):
102
111
        """Create a new instance to manage the view."""
103
112
        self._title = title
104
 
        self._existing_account_id = existing_account_id
105
 
        self._new_account_id = new_account_id
106
113
 
107
114
    # use an ugly name just so have a simlar api as found in PyQt
108
115
    #pylint: disable=C0103
130
137
    def _set_next_existing(self, view):
131
138
        """Set the next id and fire signal."""
132
139
        logger.debug('ChooseSignInController._set_next_existing')
133
 
        view.next = self._existing_account_id
 
140
        view.next = view.wizard().current_user_page_id
134
141
        view.wizard().next()
135
142
 
136
143
    def _set_next_new(self, view):
137
144
        """Set the next id and fire signal."""
138
145
        logger.debug('ChooseSignInController._set_next_new')
139
 
        view.next = self._new_account_id
 
146
        view.next = view.wizard().setup_account_page_id
140
147
        view.wizard().next()
141
148
 
142
149
 
143
150
class CurrentUserController(BackendController):
144
151
    """Controller used in the view that is used to allow the signin."""
145
152
 
146
 
    def __init__(self, backend=None, title='', app_name='', message_box=None):
 
153
    def __init__(self, backend=None, title='', message_box=None):
147
154
        """Create a new instance."""
148
155
        super(CurrentUserController, self).__init__()
149
156
        if message_box is None:
150
157
            message_box = QMessageBox
151
158
        self.message_box = message_box
152
159
        self._title = title
153
 
        self._app_name = app_name
154
160
 
155
161
    def _set_translated_strings(self, view):
156
162
        """Set the translated strings."""
157
163
        logger.debug('CurrentUserController._set_translated_strings')
158
164
        view.email_edit.setPlaceholderText(EMAIL1_ENTRY)
159
165
        view.password_edit.setPlaceholderText(PASSWORD1_ENTRY)
160
 
        view.forgot_password_label.setText(FORGOTTEN_PASSWORD_BUTTON)
 
166
        view.forgot_password_label.setText(
 
167
                                    FAKE_URL % FORGOTTEN_PASSWORD_BUTTON)
161
168
        view.sign_in_button.setText(SIGN_IN_BUTTON)
162
169
 
163
 
    def _connect_buttons(self, view, backend):
 
170
    def _connect_ui(self, view, backend):
164
171
        """Connect the buttons to perform actions."""
165
172
        logger.debug('CurrentUserController._connect_buttons')
166
173
        view.sign_in_button.clicked.connect(lambda: self.login(view, backend))
167
174
        # lets add call backs to be execute for the calls we are interested
168
175
        backend.on_login_error_cb = lambda app, error:\
169
 
                                        self.on_login_error(view, app, error)
 
176
                                        self.on_login_error(view, error)
170
177
        backend.on_logged_in_cb = lambda app, result:\
171
178
                                        self.on_logged_in(view, app, result)
 
179
        view.forgot_password_label.linkActivated.connect(lambda link:\
 
180
                                        self.on_forgotten_password(view))
172
181
 
173
182
    def login(self, view, backend):
174
183
        """Perform the login using the backend."""
175
184
        logger.debug('CurrentUserController.login')
176
185
        # grab the data from the view and call the backend
177
 
        d = backend.login(self._app_name, view.email, view.password)
178
 
        d.addErrback(lambda e: self.on_login_error(view, self._app_name, e))
 
186
        d = backend.login(view.wizard().app_name, view.email, view.password)
 
187
        d.addErrback(lambda e: self.on_login_error(view, e))
179
188
 
180
 
    def on_login_error(self, view, app_name, error):
 
189
    def on_login_error(self, view, error):
181
190
        """There was an error when login in."""
182
191
        # let the user know
183
 
        logger.error('Got error when login %s, error: %s', app_name, error)
184
 
        self.message_box.critical(view, app_name, str(error))
 
192
        logger.error('Got error when login %s, error: %s',
 
193
                     view.wizard().app_name, error)
 
194
        self.message_box.critical(view, view.wizard().app_name, str(error))
185
195
 
186
196
    @inlineCallbacks
187
197
    def on_logged_in(self, view, app_name, result):
190
200
        view.wizard().loginSuccess.emit(app_name, view.email)
191
201
        logger.debug('Wizard.loginSuccess emitted.')
192
202
 
 
203
    def on_forgotten_password(self, view):
 
204
        """Show the user the forgotten password page."""
 
205
        logger.info('Forgotten password')
 
206
        view.next = view.wizard().forgotten_password_page_id
 
207
        view.wizard().next()
 
208
 
193
209
    # use an ugly name just so have a simlar api as found in PyQt
194
210
    #pylint: disable=C0103
195
211
    @inlineCallbacks
198
214
        backend = yield self.get_backend()
199
215
        view.setTitle(self._title)
200
216
        self._set_translated_strings(view)
201
 
        self._connect_buttons(view, backend)
 
217
        self._connect_ui(view, backend)
202
218
    #pylint: enable=C0103
203
219
 
204
220
 
205
221
class SetUpAccountController(BackendController):
206
222
    """Conroller for the setup account view."""
207
223
 
208
 
    def __init__(self, tos_id=0, validation_id=1, app_name='',
209
 
                 help_message='', message_box=None):
 
224
    def __init__(self,  message_box=None):
210
225
        """Create a new instance."""
211
226
        super(SetUpAccountController, self).__init__()
212
227
        if message_box is None:
213
228
            message_box = QMessageBox
214
229
        self.message_box = message_box
215
 
        self._tos_id = tos_id
216
 
        self._validation_id = validation_id
217
 
        self._app_name = app_name
218
 
        self._help_message = help_message
219
230
 
220
231
    def _set_translated_strings(self, view):
221
232
        """Set the different gettext translated strings."""
230
241
        view.password_info_label.setText(PASSWORD_HELP)
231
242
        view.captcha_solution_edit.setPlaceholderText(CAPTCHA_SOLUTION_ENTRY)
232
243
        view.terms_and_conditions_check_box.setText(
233
 
                                    YES_TO_TC % {'app_name': self._app_name})
 
244
                            YES_TO_TC % {'app_name': view.wizard().app_name})
234
245
        view.terms_and_conditions_button.setText(TC_BUTTON)
235
246
        view.set_up_button.setText(SET_UP_ACCOUNT_BUTTON)
236
247
 
237
248
    def _set_line_edits_validations(self, view):
238
249
        """Set the validations to be performed on the edits."""
239
250
        logger.debug('SetUpAccountController._set_line_edits_validations')
240
 
        view.set_line_edit_validation_rule(view.email_edit,
241
 
                                           self.is_correct_email)
 
251
        view.set_line_edit_validation_rule(view.email_edit, is_correct_email)
242
252
        # set the validation rule for the email confirmation
243
253
        view.set_line_edit_validation_rule(view.confirm_email_edit,
244
254
                         lambda x: self.is_correct_email_confirmation(x, view))
271
281
        backend.on_captcha_generated_cb = lambda app, result:\
272
282
                                self.on_captcha_generated(view, app, result)
273
283
        backend.on_captcha_generation_error_cb = lambda app, error:\
274
 
                            self.on_captcha_generation_error(view, app, error)
 
284
                            self.on_captcha_generation_error(view, error)
275
285
        backend.on_user_registered_cb = lambda app, result:\
276
286
                            self.on_user_registered(view, app, result)
277
287
        backend.on_user_registration_error_cb = lambda app, error:\
288
298
        fd = tempfile.TemporaryFile(mode='r')
289
299
        file_name = fd.name
290
300
        view.captcha_file = file_name
291
 
        d = backend.generate_captcha(self._app_name, file_name)
 
301
        d = backend.generate_captcha(view.wizard().app_name, file_name)
292
302
        if old_file:
293
303
            d.addCallback(lambda x: os.unlink(old_file))
294
 
        d.addErrback(lambda e: self.on_captcha_generation_error(view,
295
 
                                                                self._app_name,
296
 
                                                                e))
 
304
        d.addErrback(lambda e: self.on_captcha_generation_error(view, e))
297
305
 
298
306
    def _set_titles(self, view):
299
307
        """Set the diff titles of the view."""
300
308
        logger.debug('SetUpAccountController._set_titles')
301
 
        view.setTitle(JOIN_HEADER_LABEL % {'app_name': self._app_name})
302
 
        view.setSubTitle(self._help_message)
 
309
        wizard = view.wizard()
 
310
        view.setTitle(JOIN_HEADER_LABEL % {'app_name': wizard.app_name})
 
311
        view.setSubTitle(wizard.help_text)
303
312
 
304
313
    def on_captcha_generated(self, view, app_name, result):
305
314
        """A new image was generated."""
321
330
        pixmap_image.loadFromData(string_io.getvalue())
322
331
        view.captcha_image = pixmap_image
323
332
 
324
 
    def on_captcha_generation_error(self, view, app_name, error):
 
333
    def on_captcha_generation_error(self, view, error):
325
334
        """An error ocurred."""
326
335
        logger.debug('SetUpAccountController.on_captcha_generation_error')
327
 
        self.message_box.critical(view, app_name, str(error))
 
336
        self.message_box.critical(view, view.wizard().app_name, str(error))
328
337
 
329
338
    def on_user_registration_error(self, view, app_name, error):
330
339
        """Let the user know we could not register."""
341
350
    def set_next_tos(self, view):
342
351
        """Set the tos page as the next one."""
343
352
        logger.debug('SetUpAccountController.set_next_tos')
344
 
        view.next = self._tos_id
 
353
        view.next = view.wizard().tos_page_id
345
354
        view.wizard().next()
346
355
 
347
356
    def validate_form(self, view):
348
357
        """Validate the info of the form and return an error."""
349
358
        logger.debug('SetUpAccountController.validate_form')
350
 
        if not self.is_correct_email(view.email):
351
 
            self.message_box.critical(view, self._app_name, EMAIL_INVALID)
 
359
        if not is_correct_email(view.email):
 
360
            self.message_box.critical(view, view.wizard().app_name,
 
361
                                      EMAIL_INVALID)
352
362
        if view.email_edit.text() != view.confirm_email_edit.text():
353
 
            self.message_box.critical(view, self._app_name, EMAIL_MISMATCH)
 
363
            self.message_box.critical(view, view.wizard().app_name,
 
364
                                      EMAIL_MISMATCH)
354
365
            return False
355
366
        if not is_min_required_password(str(view.password_edit.text())):
356
 
            self.message_box.critical(view, self._app_name, PASSWORD_TOO_WEAK)
 
367
            self.message_box.critical(view, view.wizard().app_name,
 
368
                                      PASSWORD_TOO_WEAK)
357
369
            return False
358
370
        if view.password_edit.text() != view.confirm_password_edit.text():
359
 
            self.message_box.critical(view, self._app_name, PASSWORD_MISMATCH)
 
371
            self.message_box.critical(view, view.wizard().app_name,
 
372
                                      PASSWORD_MISMATCH)
360
373
            return False
361
374
        return True
362
375
 
366
379
        # validate the current info of the form, try to perform the action
367
380
        # to register the user, and then move foward
368
381
        if self.validate_form(view):
369
 
            backend.register_user(self._app_name, view.email, view.password,
370
 
                                  view.captcha_id, view.captcha_solution)
371
 
            view.next = self._validation_id
 
382
            backend.register_user(view.wizard().app_name, view.email,
 
383
                                  view.password, view.captcha_id,
 
384
                                  view.captcha_solution)
 
385
            view.next = view.wizard().email_verification_page_id
372
386
            view.wizard().next()
373
387
 
374
388
    def update_password_strength(self, password, view):
411
425
class TosController(object):
412
426
    """Controller used for the tos page."""
413
427
 
414
 
    def __init__(self, title='', subtitle='', tos_url='http://www.ubuntu.com'):
 
428
    def __init__(self, title='', subtitle='', tos_url=''):
415
429
        """Create a new instance."""
416
430
        self._title = title
417
431
        self._subtitle = subtitle
474
488
    #pylint: enable=C0103
475
489
 
476
490
 
 
491
class ForgottenPasswordController(BackendController):
 
492
    """Controller used to deal with the forgotten pwd page."""
 
493
 
 
494
    def __init__(self):
 
495
        """Create a new instance."""
 
496
        super(ForgottenPasswordController, self).__init__()
 
497
 
 
498
    def _register_fields(self, view):
 
499
        """Register the fields of the wizard page."""
 
500
        view.registerField('email_address', view.email_address_line_edit)
 
501
 
 
502
    def _set_translated_strings(self, view):
 
503
        """Set the translated strings in the view."""
 
504
        view.forgotted_password_intro_label.setText(
 
505
                                    REQUEST_PASSWORD_TOKEN_LABEL % {'app_name':
 
506
                                    view.wizard().app_name})
 
507
        view.email_address_label.setText(EMAIL_LABEL)
 
508
        view.send_button.setText(RESET_PASSWORD)
 
509
        view.try_again_button.setText(TRY_AGAIN_BUTTON)
 
510
 
 
511
    def _set_enhanced_line_edit(self, view):
 
512
        """Set the extra logic to the line edits."""
 
513
        view.set_line_edit_validation_rule(view.email_address_line_edit,
 
514
                                           is_correct_email)
 
515
 
 
516
    def _connect_ui(self, view, backend):
 
517
        """Connect the diff signals from the Ui."""
 
518
        view.send_button.clicked.connect(
 
519
                            lambda: backend.request_password_reset_token(
 
520
                                                    view.wizard().app_name,
 
521
                                                    view.email_address))
 
522
        view.try_again_button.clicked.connect(lambda: self.on_try_again(view))
 
523
        # set the backend callbacks to be used
 
524
        backend.on_password_reset_token_sent_cb = lambda app, result:\
 
525
                                    self.on_password_reset_token_sent(view)
 
526
        backend.on_password_reset_error_cb = lambda app_name, error:\
 
527
                                    self.on_password_reset_error(app_name,
 
528
                                                                 error,
 
529
                                                                 view)
 
530
 
 
531
    def on_try_again(self, view):
 
532
        """Set back the widget to the initial state."""
 
533
        view.error_label.setVisible(False)
 
534
        view.try_again_widget.setVisible(False)
 
535
        view.email_widget.setVisible(True)
 
536
 
 
537
    def on_password_reset_token_sent(self, view):
 
538
        """Action taken when we managed to get the password reset done."""
 
539
        # ignore the result and move to the reset page
 
540
        view.next = view.wizard().reset_password_page_id
 
541
        view.wizard().next()
 
542
 
 
543
    def on_password_reset_error(self, app_name, error, view):
 
544
        """Action taken when there was an error requesting the reset."""
 
545
        if error['errtype'] == 'ResetPasswordTokenError':
 
546
            # the account provided is wrong, lets tell the user to try
 
547
            # again
 
548
            view.error_label.setText(REQUEST_PASSWORD_TOKEN_WRONG_EMAIL)
 
549
            view.error_label.setVisible(True)
 
550
        else:
 
551
            # ouch, I dont know what went wrong, tell the user to try later
 
552
            view.email_widget.setVisible(False)
 
553
            view.forgotted_password_intro_label.setVisible(False)
 
554
            view.try_again_wisget.setVisible(True)
 
555
            # set the error message
 
556
            view.error_label.setText(REQUEST_PASSWORD_TOKEN_TECH_ERROR)
 
557
 
 
558
    #pylint: disable=C0103
 
559
    @inlineCallbacks
 
560
    def setupUi(self, view):
 
561
        """Setup the view."""
 
562
        backend = yield self.get_backend()
 
563
        # hide the error label
 
564
        view.error_label.setVisible(False)
 
565
        view.try_again_widget.setVisible(False)
 
566
        self._set_translated_strings(view)
 
567
        self._connect_ui(view, backend)
 
568
        self._set_enhanced_line_edit(view)
 
569
        self._register_fields(view)
 
570
    #pylint: enable=C0103
 
571
 
 
572
 
 
573
class ResetPasswordController(BackendController):
 
574
    """Controller used to deal with reseintg the password."""
 
575
 
 
576
    def __init__(self):
 
577
        """Create a new instance."""
 
578
        super(ResetPasswordController, self).__init__()
 
579
 
 
580
    def _set_translated_strings(self, view):
 
581
        """Translate the diff strings used in the app."""
 
582
        view.reset_code_line_edit.setPlaceholderText(RESET_CODE_ENTRY)
 
583
        view.password_line_edit.setPlaceholderText(PASSWORD1_ENTRY)
 
584
        view.confirm_password_line_edit.setPlaceholderText(PASSWORD2_ENTRY)
 
585
        view.reset_password_button.setText(RESET_PASSWORD)
 
586
        view.setSubTitle(PASSWORD_HELP)
 
587
 
 
588
    def _connect_ui(self, view, backend):
 
589
        """Connect the different ui signals."""
 
590
        view.reset_password_button.clicked.connect(
 
591
                                lambda: self.set_new_password(view, backend))
 
592
        backend.on_password_changed_cb = lambda app, result:\
 
593
                                            self.on_password_changed(app,
 
594
                                                                     result,
 
595
                                                                     view)
 
596
        backend.on_password_change_error_cb = lambda app, error:\
 
597
                                        self.on_password_change_error(app,
 
598
                                                                      error,
 
599
                                                                      view)
 
600
 
 
601
    def _add_line_edits_validations(self, view):
 
602
        """Add the validations to be use by the line edits."""
 
603
        view.set_line_edit_validation_rule(view.password_line_edit,
 
604
                                           is_min_required_password)
 
605
        view.set_line_edit_validation_rule(view.confirm_password_line_edit,
 
606
                      lambda x: self.is_correct_password_confirmation(x, view))
 
607
        # same as the above case, lets connect a signal to a signal
 
608
        view.password_line_edit.textChanged.connect(
 
609
                              view.confirm_password_line_edit.textChanged.emit)
 
610
 
 
611
    def on_password_changed(self, app_name, result, view):
 
612
        """Let user know that the password was changed."""
 
613
 
 
614
    def on_password_change_error(self, app_name, error, view):
 
615
        """Let the user know that there was an error."""
 
616
 
 
617
    def set_new_password(self, view, backend):
 
618
        """Request a new password to be set."""
 
619
        app_name = view.wizard().app_name
 
620
        email = str(view.wizard().field('email_address').toString())
 
621
        code = view.reset_code
 
622
        logger.info('Settig new password for %s and email %s with code %s',
 
623
                    app_name, email, code)
 
624
        backend.set_new_password(app_name, email, code, view.password)
 
625
 
 
626
    def is_correct_password_confirmation(self, password, view):
 
627
        """Return if the password is correct."""
 
628
        return view.password_line_edit.text() == password
 
629
 
 
630
    #pylint: disable=C0103
 
631
    @inlineCallbacks
 
632
    def setupUi(self, view):
 
633
        """Setup the view."""
 
634
        backend = yield self.get_backend()
 
635
        self._set_translated_strings(view)
 
636
        self._connect_ui(view, backend)
 
637
        self._add_line_edits_validations(view)
 
638
    #pylint: enable=C0103
 
639
 
 
640
 
477
641
class SuccessController(object):
478
642
    """Controller used for the success page."""
479
643
 
491
655
class UbuntuSSOWizardController(object):
492
656
    """Controller used for the overall wizard."""
493
657
 
494
 
    def __init__(self, app_name='', login_success_callback=NO_OP,
 
658
    def __init__(self, login_success_callback=NO_OP,
495
659
                 registration_success_callback=NO_OP,
496
660
                 user_cancellation_callback=NO_OP):
497
661
        """Create a new instance."""
498
 
        self.app_name = app_name
499
662
        self.login_success_callback = login_success_callback
500
663
        self.registration_success_callback = registration_success_callback
501
664
        self.user_cancellation_callback = user_cancellation_callback
503
666
    def on_user_cancelation(self, view):
504
667
        """Process the cancel action."""
505
668
        logger.debug('UbuntuSSOWizardController.on_user_cancelation')
506
 
        self.user_cancellation_callback(self.app_name)
 
669
        self.user_cancellation_callback(view.app_name)
507
670
        view.close()
508
671
 
509
672
    @inlineCallbacks