1
# -*- coding: utf-8 -*-
3
# Copyright 2011-2012 Canonical Ltd.
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License version 3, as published
7
# by the Free Software Foundation.
9
# This program is distributed in the hope that it will be useful, but
10
# WITHOUT ANY WARRANTY; without even the implied warranties of
11
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12
# PURPOSE. See the GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License along
15
# with this program. If not, see <http://www.gnu.org/licenses/>.
16
"""Controllers with the logic of the UI."""
23
# pylint: disable=F0401
28
# pylint: enable=F0401
30
from PyQt4.QtGui import QMessageBox, QWizard, QPixmap
31
from twisted.internet import defer
33
from ubuntu_sso import main, NO_OP
34
from ubuntu_sso.logger import setup_logging
35
from ubuntu_sso.utils.ui import (
37
CAPTCHA_REQUIRED_ERROR,
38
CAPTCHA_SOLUTION_ENTRY,
45
EXISTING_ACCOUNT_CHOICE_BUTTON,
46
FORGOTTEN_PASSWORD_BUTTON,
47
is_min_required_password,
58
REQUEST_PASSWORD_TOKEN_LABEL,
60
REQUEST_PASSWORD_TOKEN_WRONG_EMAIL,
61
REQUEST_PASSWORD_TOKEN_TECH_ERROR,
62
SET_UP_ACCOUNT_CHOICE_BUTTON,
72
ERROR_EMAIL_TOKEN = 'email_token'
73
ERROR_MESSAGE = 'message'
74
ERROR_PASSWORD = 'password'
75
logger = setup_logging('ubuntu_sso.controllers')
78
# Based on the gtk implementation
79
def _build_general_error_message(errordict):
80
"""Build a user-friendly error message from the errordict."""
82
if isinstance(errordict, collections.Mapping):
83
msg1 = errordict.get(ERROR_ALL)
84
msg2 = errordict.get(ERROR_MESSAGE)
86
# See the errordict in LP: 828417
87
msg2 = errordict.get('error_message')
88
if msg1 is not None and msg2 is not None:
89
result = '\n'.join((msg1, msg2))
90
elif msg1 is not None:
92
elif msg2 is not None:
95
if 'errtype' in errordict:
96
del errordict['errtype']
98
[('%s: %s' % (k, v)) for k, v in errordict.iteritems()])
100
result = repr(errordict)
104
class BackendController(object):
105
"""Represent a controller that talks with the sso self.backend."""
107
def __init__(self, title='', subtitle=''):
108
"""Create a new instance."""
112
self._subtitle = subtitle
114
@defer.inlineCallbacks
115
def get_backend(self):
116
"""Return the backend used by the controller."""
117
if self.backend is None:
118
client = yield main.get_sso_client()
119
self.backend = client.sso_login
120
defer.returnValue(self.backend)
122
#pylint: disable=C0103
123
def pageInitialized(self):
124
"""Call to prepare the page just before it is shown."""
125
#pylint: enable=C0103
128
class ChooseSignInController(BackendController):
129
"""Controlled to the ChooseSignIn view/widget."""
131
def __init__(self, title='', subtitle=''):
132
"""Create a new instance to manage the view."""
133
super(ChooseSignInController, self).__init__(title, subtitle)
136
# use an ugly name just so have a simlar api as found in PyQt
137
# pylint: disable=C0103
138
def setupUi(self, view):
139
"""Perform the required actions to set up the ui."""
141
self._set_up_translated_strings()
142
self.view.header.set_title(self._title)
143
self.view.header.set_subtitle(self._subtitle)
144
self._connect_buttons()
145
# pylint: enable=C0103
147
def _set_up_translated_strings(self):
148
"""Set the correct strings for the UI."""
149
logger.debug('ChooseSignInController._set_up_translated_strings')
150
self.view.ui.existing_account_button.setText(
151
EXISTING_ACCOUNT_CHOICE_BUTTON)
152
self.view.ui.setup_account_button.setText(
153
SET_UP_ACCOUNT_CHOICE_BUTTON)
155
def _connect_buttons(self):
156
"""Connect the buttons to the actions to perform."""
157
logger.debug('ChooseSignInController._connect_buttons')
158
self.view.ui.existing_account_button.clicked.connect(
159
self._set_next_existing)
160
self.view.ui.setup_account_button.clicked.connect(self._set_next_new)
162
def _set_next_existing(self):
163
"""Set the next id and fire signal."""
164
logger.debug('ChooseSignInController._set_next_existing')
165
self.view.next = self.view.wizard().current_user_page_id
166
self.view.wizard().next()
168
def _set_next_new(self):
169
"""Set the next id and fire signal."""
170
logger.debug('ChooseSignInController._set_next_new')
171
self.view.next = self.view.wizard().setup_account_page_id
172
self.view.wizard().next()
175
class CurrentUserController(BackendController):
176
"""Controller used in the view that is used to allow the signin."""
178
def __init__(self, backend=None, title='', subtitle='', message_box=None):
179
"""Create a new instance."""
180
super(CurrentUserController, self).__init__(title, subtitle)
181
if message_box is None:
182
message_box = QMessageBox
183
self.message_box = message_box
185
def _set_translated_strings(self):
186
"""Set the translated strings."""
187
logger.debug('CurrentUserController._set_translated_strings')
188
self.view.ui.email_label.setText(EMAIL_LABEL)
189
self.view.ui.password_label.setText(LOGIN_PASSWORD_LABEL)
190
self.view.ui.forgot_password_label.setText(FORGOTTEN_PASSWORD_BUTTON)
191
self.view.ui.sign_in_button.setText(SIGN_IN_BUTTON)
193
def _connect_ui(self):
194
"""Connect the buttons to perform actions."""
195
logger.debug('CurrentUserController._connect_buttons')
196
self.view.ui.sign_in_button.clicked.connect(self.login)
197
# lets add call backs to be execute for the calls we are interested
198
self.backend.on_login_error_cb = lambda app, error:\
199
self.on_login_error(error)
200
self.backend.on_logged_in_cb = self.on_logged_in
201
self.view.ui.forgot_password_label.linkActivated.connect(
202
self.on_forgotten_password)
203
self.view.ui.email_edit.textChanged.connect(self._validate)
204
self.view.ui.password_edit.textChanged.connect(self._validate)
207
"""Perform input validation."""
209
if not is_correct_email(unicode(self.view.ui.email_edit.text())) or \
210
not unicode(self.view.ui.password_edit.text()):
212
self.view.ui.sign_in_button.setEnabled(valid)
213
self.view.ui.sign_in_button.setProperty("DisabledState",
214
not self.view.ui.sign_in_button.isEnabled())
215
self.view.ui.sign_in_button.style().unpolish(
216
self.view.ui.sign_in_button)
217
self.view.ui.sign_in_button.style().polish(
218
self.view.ui.sign_in_button)
221
"""Perform the login using the self.backend."""
222
logger.debug('CurrentUserController.login')
223
# grab the data from the view and call the backend
224
email = unicode(self.view.ui.email_edit.text())
225
password = unicode(self.view.ui.password_edit.text())
226
d = self.backend.login(self.view.wizard().app_name, email, password)
227
d.addErrback(self.on_login_error)
229
def on_login_error(self, error):
230
"""There was an error when login in."""
232
logger.error('Got error when login %s, error: %s',
233
self.view.wizard().app_name, error)
234
if isinstance(error, collections.Mapping) and \
235
error.get('errtype', None) == 'UserNotValidated':
236
self.view.setField('email_address', self.view.ui.email_edit.text())
237
self.view.setField('password', self.view.ui.password_edit.text())
238
app_name = self.view.wizard().app_name
239
self.view.wizard().registrationIncomplete.emit(
240
app_name, error['message'])
242
self.message_box.critical(_build_general_error_message(error),
245
def on_logged_in(self, app_name, result):
246
"""We managed to log in."""
247
logger.info('Logged in for %s', app_name)
248
email = unicode(self.view.ui.email_edit.text())
249
self.view.wizard().loginSuccess.emit(app_name, email)
250
logger.debug('Wizard.loginSuccess emitted.')
252
def on_forgotten_password(self):
253
"""Show the user the forgotten password page."""
254
logger.info('Forgotten password')
255
email = unicode(self.view.ui.email_edit.text())
256
self.view.wizard().forgotten.ui.email_line_edit.setText(email)
257
self.view.next = self.view.wizard().forgotten_password_page_id
258
self.view.wizard().next()
260
# use an ugly name just so have a simlar api as found in PyQt
261
#pylint: disable=C0103
262
@defer.inlineCallbacks
263
def setupUi(self, view):
264
"""Setup the view."""
266
self.backend = yield self.get_backend()
267
self.view.header.set_title(self._title)
268
self.view.header.set_subtitle(self._subtitle)
269
self._set_translated_strings()
271
#pylint: enable=C0103
274
class SetUpAccountController(BackendController):
275
"""Controller for the setup account view."""
277
def __init__(self, message_box=None, title='', subtitle=''):
278
"""Create a new instance."""
279
super(SetUpAccountController, self).__init__(title, subtitle)
280
if message_box is None:
281
message_box = QMessageBox
282
self.message_box = message_box
284
def _set_translated_strings(self):
285
"""Set the different gettext translated strings."""
286
logger.debug('SetUpAccountController._set_translated_strings')
287
# set the translated string
288
self.view.ui.name_label.setText(NAME_ENTRY)
289
self.view.ui.email_label.setText(EMAIL1_ENTRY)
290
self.view.ui.confirm_email_label.setText(EMAIL2_ENTRY)
291
self.view.ui.password_label.setText(PASSWORD1_ENTRY)
292
self.view.ui.confirm_password_label.setText(PASSWORD2_ENTRY)
293
self.view.ui.password_info_label.setText(PASSWORD_HELP)
294
self.view.ui.captcha_solution_edit.setPlaceholderText(
295
CAPTCHA_SOLUTION_ENTRY)
297
def _set_line_edits_validations(self):
298
"""Set the validations to be performed on the edits."""
299
logger.debug('SetUpAccountController._set_line_edits_validations')
300
self.view.set_line_edit_validation_rule(self.view.ui.email_edit,
302
# set the validation rule for the email confirmation
303
self.view.set_line_edit_validation_rule(
304
self.view.ui.confirm_email_edit,
305
self.is_correct_email_confirmation)
306
# connect the changed text of the password to trigger a changed text
307
# in the confirm so that the validation is redone
308
self.view.ui.email_edit.textChanged.connect(
309
self.view.ui.confirm_email_edit.textChanged.emit)
310
self.view.set_line_edit_validation_rule(self.view.ui.password_edit,
311
is_min_required_password)
312
self.view.set_line_edit_validation_rule(
313
self.view.ui.confirm_password_edit,
314
self.is_correct_password_confirmation)
315
# same as the above case, lets connect a signal to a signal
316
self.view.ui.password_edit.textChanged.connect(
317
self.view.ui.confirm_password_edit.textChanged.emit)
319
def _connect_ui_elements(self):
320
"""Set the connection of signals."""
321
logger.debug('SetUpAccountController._connect_ui_elements')
322
self.view.ui.refresh_label.linkActivated.connect(lambda url: \
323
self._refresh_captcha())
324
# set the callbacks for the captcha generation
325
self.backend.on_captcha_generated_cb = self.on_captcha_generated
326
self.backend.on_captcha_generation_error_cb = lambda app, error: \
327
self.on_captcha_generation_error(error)
328
self.backend.on_user_registered_cb = self.on_user_registered
329
self.backend.on_user_registration_error_cb = \
330
self.on_user_registration_error
331
# We need to check if we enable the button on many signals
332
self.view.ui.name_edit.textEdited.connect(self._enable_setup_button)
333
self.view.ui.email_edit.textEdited.connect(self._enable_setup_button)
334
self.view.ui.confirm_email_edit.textEdited.connect(
335
self._enable_setup_button)
336
self.view.ui.password_edit.textEdited.connect(
337
self._enable_setup_button)
338
self.view.ui.confirm_password_edit.textEdited.connect(
339
self._enable_setup_button)
340
self.view.ui.captcha_solution_edit.textEdited.connect(
341
self._enable_setup_button)
342
self.view.terms_checkbox.stateChanged.connect(
343
self._enable_setup_button)
345
def _enable_setup_button(self):
346
"""Only enable the setup button if the form is valid."""
347
name = unicode(self.view.ui.name_edit.text()).strip()
348
email = unicode(self.view.ui.email_edit.text())
349
confirm_email = unicode(self.view.ui.confirm_email_edit.text())
350
password = unicode(self.view.ui.password_edit.text())
351
confirm_password = unicode(self.view.ui.confirm_password_edit.text())
352
captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
354
# Check for len(name) > 0 to ensure that a bool is assigned to enabled
355
enabled = self.view.terms_checkbox.isChecked() and \
356
len(captcha_solution) > 0 and \
357
is_min_required_password(password) and \
358
password == confirm_password and is_correct_email(email) and \
359
email == confirm_email and len(name) > 0
361
self.view.set_up_button.setEnabled(enabled)
362
self.view.set_up_button.setProperty("DisabledState", not enabled)
363
self.view.set_up_button.style().unpolish(self.view.set_up_button)
364
self.view.set_up_button.style().polish(self.view.set_up_button)
366
def _refresh_captcha(self):
367
"""Refresh the captcha image shown in the ui."""
368
logger.debug('SetUpAccountController._refresh_captcha')
369
# lets clean behind us, do we have the old file arround?
371
if self.view.captcha_file and os.path.exists(self.view.captcha_file):
372
old_file = self.view.captcha_file
373
fd = tempfile.TemporaryFile(mode='r')
375
self.view.captcha_file = file_name
376
d = self.backend.generate_captcha(self.view.wizard().app_name,
379
d.addCallback(lambda x: os.unlink(old_file))
380
d.addErrback(self.on_captcha_generation_error)
381
self.view.on_captcha_refreshing()
383
def _set_titles(self):
384
"""Set the diff titles of the view."""
385
logger.debug('SetUpAccountController._set_titles')
386
self.view.header.set_title(
387
JOIN_HEADER_LABEL % {'app_name': self.view.wizard().app_name})
388
self.view.header.set_subtitle(self.view.wizard().help_text)
390
def _register_fields(self):
391
"""Register the diff fields of the Ui."""
392
self.view.registerField('email_address', self.view.ui.email_edit)
393
self.view.registerField('password', self.view.ui.password_edit)
395
def on_captcha_generated(self, app_name, result):
396
"""A new image was generated."""
397
logger.debug('SetUpAccountController.on_captcha_generated for %r '
398
'(captcha id %r, filename %r).',
399
app_name, result, self.view.captcha_file)
400
self.view.captcha_id = result
401
# HACK: First, let me apologize before hand, you can mention my mother
402
# if needed I would do the same (mandel)
403
# In an ideal world we could use the Qt plug-in for the images so that
404
# we could load jpgs etc.. but this won't work when the app has been
405
# brozen win py2exe using bundle_files=1
406
# The main issue is that Qt will complain about the thread not being
407
# the correct one when performing a moveToThread operation which is
408
# done either by a setParent or something within the qtreactor, PIL
409
# in this case does solve the issue. Sorry :(
410
pil_image = Image.open(self.view.captcha_file)
411
string_io = StringIO.StringIO()
412
pil_image.save(string_io, format='png')
413
pixmap_image = QPixmap()
414
pixmap_image.loadFromData(string_io.getvalue())
415
self.view.captcha_image = pixmap_image
416
self.view.on_captcha_refresh_complete()
418
def on_captcha_generation_error(self, error):
419
"""An error ocurred."""
420
logger.debug('SetUpAccountController.on_captcha_generation_error')
421
self.message_box.critical(CAPTCHA_LOAD_ERROR, self.view)
422
self.view.on_captcha_refresh_complete()
424
def on_user_registration_error(self, app_name, error):
425
"""Let the user know we could not register."""
426
logger.debug('SetUpAccountController.on_user_registration_error')
427
# errors are returned as a dict with the data we want to show.
428
self._refresh_captcha()
429
msg = error.pop(ERROR_EMAIL, None)
431
self.view.set_error_message(self.view.ui.email_assistance, msg)
432
self.message_box.critical(_build_general_error_message(error),
435
def on_user_registered(self, app_name, result):
436
"""Execute when the user did register."""
437
logger.debug('SetUpAccountController.on_user_registered')
438
self.view.next = self.view.wizard().email_verification_page_id
439
self.view.wizard().next()
441
def validate_form(self):
442
"""Validate the info of the form and return an error."""
443
logger.debug('SetUpAccountController.validate_form')
444
name = unicode(self.view.ui.name_edit.text()).strip()
445
email = unicode(self.view.ui.email_edit.text())
446
confirm_email = unicode(self.view.ui.confirm_email_edit.text())
447
password = unicode(self.view.ui.password_edit.text())
448
confirm_password = unicode(self.view.ui.confirm_password_edit.text())
449
captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
454
self.view.set_error_message(self.view.ui.name_assistance,
456
if not is_correct_email(email):
458
self.view.set_error_message(self.view.ui.email_assistance,
460
if email != confirm_email:
462
self.view.set_error_message(self.view.ui.confirm_email_assistance,
464
if not is_min_required_password(password):
465
messages.append(PASSWORD_TOO_WEAK)
466
if password != confirm_password:
467
messages.append(PASSWORD_MISMATCH)
468
if not captcha_solution:
469
messages.append(CAPTCHA_REQUIRED_ERROR)
470
if len(messages) > 0:
472
self.message_box.critical('\n'.join(messages), self.view)
475
def set_next_validation(self):
476
"""Set the validation as the next page."""
477
logger.debug('SetUpAccountController.set_next_validation')
478
email = unicode(self.view.ui.email_edit.text())
479
password = unicode(self.view.ui.password_edit.text())
480
name = unicode(self.view.ui.name_edit.text())
481
captcha_id = self.view.captcha_id
482
captcha_solution = unicode(self.view.ui.captcha_solution_edit.text())
483
# validate the current info of the form, try to perform the action
484
# to register the user, and then move foward
485
if self.validate_form():
486
self.backend.register_user(self.view.wizard().app_name, email,
487
password, name, captcha_id,
489
# Update validation page's title, which contains the email
490
p_id = self.view.wizard().email_verification_page_id
491
self.view.wizard().page(p_id).controller.set_titles()
493
def is_correct_email(self, email_address):
494
"""Return if the email is correct."""
495
logger.debug('SetUpAccountController.is_correct_email')
496
return '@' in email_address
498
def is_correct_email_confirmation(self, email_address):
499
"""Return that the email is the same."""
500
logger.debug('SetUpAccountController.is_correct_email_confirmation')
501
return unicode(self.view.ui.email_edit.text()) == email_address
503
def is_correct_password_confirmation(self, password):
504
"""Return that the passwords are correct."""
505
logger.debug('SetUpAccountController.is_correct_password_confirmation')
506
return unicode(self.view.ui.password_edit.text()) == password
508
#pylint: disable=C0103
509
@defer.inlineCallbacks
510
def setupUi(self, view):
511
"""Setup the view."""
513
# request the backend to be used with the ui
514
self.backend = yield self.get_backend()
515
self._connect_ui_elements()
516
self._refresh_captcha()
518
self.view.header.set_title(self._title)
519
self.view.header.set_subtitle(self._subtitle)
520
self._set_translated_strings()
521
self._set_line_edits_validations()
522
self._register_fields()
523
#pylint: enable=C0103
526
class EmailVerificationController(BackendController):
527
"""Controller used for the verification page."""
529
def __init__(self, message_box=None, title='', subtitle=''):
530
"""Create a new instance."""
531
super(EmailVerificationController, self).__init__(title, subtitle)
532
if message_box is None:
533
message_box = QMessageBox
534
self.message_box = message_box
536
def _set_translated_strings(self):
537
"""Set the trnaslated strings."""
538
logger.debug('EmailVerificationController._set_translated_strings')
540
def _connect_ui_elements(self):
541
"""Set the connection of signals."""
542
logger.debug('EmailVerificationController._connect_ui_elements')
543
self.view.ui.verification_code_edit.textChanged.connect(
545
self.view.next_button.clicked.connect(self.validate_email)
546
self.backend.on_email_validated_cb = lambda app, result: \
547
self.on_email_validated(app)
548
self.backend.on_email_validation_error_cb = \
549
self.on_email_validation_error
551
def validate_form(self):
552
"""Check the state of the form."""
553
code = self.view.verification_code.strip()
554
enabled = len(code) > 0
555
self.view.next_button.setEnabled(enabled)
556
self.view.next_button.setProperty('DisabledState',
557
not self.view.next_button.isEnabled())
558
self.view.next_button.style().unpolish(
559
self.view.next_button)
560
self.view.next_button.style().polish(
561
self.view.next_button)
563
def _set_titles(self):
564
"""Set the different titles."""
565
logger.debug('EmailVerificationController._set_titles')
566
self.view.header.set_title(VERIFY_EMAIL_TITLE)
567
self.view.header.set_subtitle(VERIFY_EMAIL_CONTENT % {
568
"app_name": self.view.wizard().app_name,
569
"email": self.view.wizard().field("email_address").toString(),
572
def set_titles(self):
573
"""This class needs to have a public set_titles.
575
Since the subtitle contains data that is only known after SetupAccount
576
and _set_titles is only called on initialization.
580
#pylint: disable=C0103
581
@defer.inlineCallbacks
582
def setupUi(self, view):
583
"""Setup the view."""
585
self.backend = yield self.get_backend()
587
self._set_translated_strings()
588
self._connect_ui_elements()
589
#pylint: enable=C0103
591
def validate_email(self):
592
"""Call the next action."""
593
logger.debug('EmailVerificationController.validate_email')
594
email = unicode(self.view.wizard().field('email_address').toString())
595
password = unicode(self.view.wizard().field('password').toString())
596
code = unicode(self.view.ui.verification_code_edit.text())
597
self.backend.validate_email(self.view.wizard().app_name, email,
600
def on_email_validated(self, app_name):
601
"""Signal thrown after the email is validated."""
602
logger.info('EmailVerificationController.on_email_validated')
603
email = self.view.wizard().field('email_address').toString()
604
self.view.wizard().registrationSuccess.emit(app_name, email)
606
def on_email_validation_error(self, app_name, error):
607
"""Signal thrown when there's a problem validating the email."""
608
msg = error.pop(ERROR_EMAIL_TOKEN, '')
609
msg += _build_general_error_message(error)
610
self.message_box.critical(msg, self.view)
612
#pylint: disable=C0103
613
def pageInitialized(self):
614
"""Called to prepare the page just before it is shown."""
615
self.view.next_button.setDefault(True)
616
self.view.next_button.setEnabled(False)
617
self.view.next_button.setProperty('DisabledState',
618
not self.view.next_button.isEnabled())
619
self.view.next_button.style().unpolish(
620
self.view.next_button)
621
self.view.next_button.style().polish(
622
self.view.next_button)
623
#pylint: enable=C0103
626
class ErrorController(BackendController):
627
"""Controller used for the error page."""
629
def __init__(self, title='', subtitle=''):
630
"""Create a new instance."""
631
super(ErrorController, self).__init__(title, subtitle)
634
#pylint: disable=C0103
635
def setupUi(self, view):
636
"""Setup the view."""
639
self.view.ui.error_message_label.setText(ERROR)
640
self.view.header.set_title(self._title)
641
self.view.header.set_subtitle(self._subtitle)
642
#pylint: enable=C0103
645
class ForgottenPasswordController(BackendController):
646
"""Controller used to deal with the forgotten pwd page."""
648
def __init__(self, message_box=None, title='', subtitle=''):
649
"""Create a new instance."""
650
super(ForgottenPasswordController, self).__init__()
651
if message_box is None:
652
message_box = QMessageBox
653
self.message_box = message_box
654
super(ForgottenPasswordController, self).__init__(title, subtitle)
656
#pylint: disable=C0103
657
def pageInitialized(self):
658
"""Set the initial state of ForgottenPassword page."""
659
self.view.send_button.setDefault(True)
660
enabled = not self.view.ui.email_line_edit.text().isEmpty()
661
self.view.send_button.setEnabled(enabled)
662
# The style from this property come from the Wizard
663
self.view.send_button.setProperty("DisabledState",
664
not self.view.send_button.isEnabled())
665
self.view.send_button.style().unpolish(
666
self.view.send_button)
667
self.view.send_button.style().polish(
668
self.view.send_button)
669
#pylint: enable=C0103
671
def _register_fields(self):
672
"""Register the fields of the wizard page."""
673
self.view.registerField('email_address',
674
self.view.email_address_line_edit)
676
def _set_translated_strings(self):
677
"""Set the translated strings in the view."""
678
self.view.forgotted_password_intro_label.setText(
679
REQUEST_PASSWORD_TOKEN_LABEL % {'app_name':
680
self.view.wizard().app_name})
681
self.view.email_address_label.setText(EMAIL_LABEL)
682
self.view.send_button.setText(RESET_PASSWORD)
683
self.view.try_again_button.setText(TRY_AGAIN_BUTTON)
685
def _set_enhanced_line_edit(self):
686
"""Set the extra logic to the line edits."""
687
self.view.set_line_edit_validation_rule(
688
self.view.email_address_line_edit,
691
def _connect_ui(self):
692
"""Connect the diff signals from the Ui."""
693
self.view.email_address_line_edit.textChanged.connect(self._validate)
694
self.view.send_button.clicked.connect(
695
lambda: self.backend.request_password_reset_token(
696
self.view.wizard().app_name,
697
self.view.email_address))
698
self.view.try_again_button.clicked.connect(self.on_try_again)
699
# set the backend callbacks to be used
700
self.backend.on_password_reset_token_sent_cb = lambda app, result:\
701
self.on_password_reset_token_sent()
702
self.backend.on_password_reset_error_cb = self.on_password_reset_error
705
"""Validate that we have an email."""
706
email = unicode(self.view.email_address_line_edit.text())
707
self.view.send_button.setEnabled(is_correct_email(email))
708
self.view.send_button.setProperty("DisabledState",
709
not self.view.send_button.isEnabled())
710
self.view.send_button.style().unpolish(
711
self.view.send_button)
712
self.view.send_button.style().polish(
713
self.view.send_button)
715
def on_try_again(self):
716
"""Set back the widget to the initial state."""
717
self.view.try_again_widget.setVisible(False)
718
self.view.email_widget.setVisible(True)
720
def on_password_reset_token_sent(self):
721
"""Action taken when we managed to get the password reset done."""
722
# ignore the result and move to the reset page
723
self.view.next = self.view.wizard().reset_password_page_id
724
self.view.wizard().next()
726
def on_password_reset_error(self, app_name, error):
727
"""Action taken when there was an error requesting the reset."""
728
# set the error message
729
msg = REQUEST_PASSWORD_TOKEN_TECH_ERROR
730
if error['errtype'] == 'ResetPasswordTokenError':
731
# the account provided is wrong, lets tell the user to try
733
msg = REQUEST_PASSWORD_TOKEN_WRONG_EMAIL
735
# ouch, I dont know what went wrong, tell the user to try later
736
self.view.email_widget.setVisible(False)
737
self.view.forgotted_password_intro_label.setVisible(False)
738
self.view.try_again_widget.setVisible(True)
739
self.message_box.critical(msg, self.view)
741
#pylint: disable=C0103
742
@defer.inlineCallbacks
743
def setupUi(self, view):
744
"""Setup the view."""
746
self.backend = yield self.get_backend()
747
# hide the error label
748
self.view.try_again_widget.setVisible(False)
749
self._set_translated_strings()
751
self._set_enhanced_line_edit()
752
self._register_fields()
753
self.view.header.set_title(self._title)
754
self.view.header.set_subtitle(self._subtitle)
755
#pylint: enable=C0103
758
class ResetPasswordController(BackendController):
759
"""Controller used to deal with reseintg the password."""
761
def __init__(self, title='', subtitle='', message_box=None):
762
"""Create a new instance."""
763
if message_box is None:
764
message_box = QMessageBox
765
self.message_box = message_box
766
super(ResetPasswordController, self).__init__(title, subtitle)
768
#pylint: disable=C0103
769
def pageInitialized(self):
770
"""Set the initial state of ForgottenPassword page."""
771
self.view.ui.reset_password_button.setDefault(True)
772
self.view.ui.reset_password_button.setEnabled(False)
773
# The style from this property come from the Wizard
774
self.view.ui.reset_password_button.setProperty("DisabledState",
775
not self.view.ui.reset_password_button.isEnabled())
776
self.view.ui.reset_password_button.style().unpolish(
777
self.view.ui.reset_password_button)
778
self.view.ui.reset_password_button.style().polish(
779
self.view.ui.reset_password_button)
780
#pylint: enable=C0103
782
def _set_translated_strings(self):
783
"""Translate the diff strings used in the app."""
784
self.view.ui.reset_password_button.setText(RESET_PASSWORD)
785
self.view.setSubTitle(PASSWORD_HELP)
787
def _connect_ui(self):
788
"""Connect the different ui signals."""
789
self.view.ui.reset_password_button.clicked.connect(
790
self.set_new_password)
791
self.backend.on_password_changed_cb = self.on_password_changed
792
self.backend.on_password_change_error_cb = \
793
self.on_password_change_error
794
self.view.ui.reset_code_line_edit.textChanged.connect(self._validate)
795
self.view.ui.password_line_edit.textChanged.connect(self._validate)
796
self.view.ui.confirm_password_line_edit.textChanged.connect(
800
"""Enable the submit button if data is valid."""
802
code = unicode(self.view.ui.reset_code_line_edit.text())
803
password = unicode(self.view.ui.password_line_edit.text())
804
confirm_password = unicode(
805
self.view.ui.confirm_password_line_edit.text())
806
if not is_min_required_password(password):
808
elif not self.is_correct_password_confirmation(confirm_password):
812
self.view.ui.reset_password_button.setEnabled(enabled)
813
self.view.ui.reset_password_button.setProperty("DisabledState",
814
not self.view.ui.reset_password_button.isEnabled())
815
self.view.ui.reset_password_button.style().unpolish(
816
self.view.ui.reset_password_button)
817
self.view.ui.reset_password_button.style().polish(
818
self.view.ui.reset_password_button)
820
def _add_line_edits_validations(self):
821
"""Add the validations to be use by the line edits."""
822
self.view.set_line_edit_validation_rule(
823
self.view.ui.password_line_edit,
824
is_min_required_password)
825
self.view.set_line_edit_validation_rule(
826
self.view.ui.confirm_password_line_edit,
827
self.is_correct_password_confirmation)
828
# same as the above case, lets connect a signal to a signal
829
self.view.ui.password_line_edit.textChanged.connect(
830
self.view.ui.confirm_password_line_edit.textChanged.emit)
832
def on_password_changed(self, app_name, result):
833
"""Let user know that the password was changed."""
834
email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
835
self.view.wizard().current_user.ui.email_edit.setText(email)
836
self.view.wizard().overlay.hide()
837
current_user_id = self.view.wizard().current_user_page_id
838
visited_pages = self.view.wizard().visitedPages()
839
for index in reversed(visited_pages):
840
if index == current_user_id:
842
self.view.wizard().back()
844
def on_password_change_error(self, app_name, error):
845
"""Let the user know that there was an error."""
846
logger.error('Got error changing password for %s, error: %s',
847
self.view.wizard().app_name, error)
848
self.message_box.critical(_build_general_error_message(error),
851
def set_new_password(self):
852
"""Request a new password to be set."""
853
app_name = self.view.wizard().app_name
854
email = unicode(self.view.wizard().forgotten.ui.email_line_edit.text())
855
code = unicode(self.view.ui.reset_code_line_edit.text())
856
password = unicode(self.view.ui.password_line_edit.text())
857
logger.info('Setting new password for %r and email %r with code %r',
858
app_name, email, code)
859
self.backend.set_new_password(app_name, email, code, password)
861
def is_correct_password_confirmation(self, password):
862
"""Return if the password is correct."""
863
return unicode(self.view.ui.password_line_edit.text()) == password
865
#pylint: disable=C0103
866
@defer.inlineCallbacks
867
def setupUi(self, view):
868
"""Setup the view."""
870
self.backend = yield self.get_backend()
871
self._set_translated_strings()
873
self._add_line_edits_validations()
874
self.view.header.set_title(self._title)
875
self.view.header.set_subtitle(self._subtitle)
876
#pylint: enable=C0103
879
class SuccessController(BackendController):
880
"""Controller used for the success page."""
882
def __init__(self, title='', subtitle=''):
883
"""Create a new instance."""
884
super(SuccessController, self).__init__(title, subtitle)
887
#pylint: disable=C0103
888
def setupUi(self, view):
889
"""Setup the view."""
892
self.view.header.set_title(self._title)
893
self.view.header.set_subtitle(self._subtitle)
894
#pylint: enable=C0103
897
class UbuntuSSOWizardController(object):
898
"""Controller used for the overall wizard."""
900
def __init__(self, login_success_callback=NO_OP,
901
registration_success_callback=NO_OP,
902
user_cancellation_callback=NO_OP):
903
"""Create a new instance."""
905
self.login_success_callback = login_success_callback
906
self.registration_success_callback = registration_success_callback
907
self.user_cancellation_callback = user_cancellation_callback
909
def on_user_cancelation(self):
910
"""Process the cancel action."""
911
logger.debug('UbuntuSSOWizardController.on_user_cancelation')
912
self.user_cancellation_callback(self.view.app_name)
915
@defer.inlineCallbacks
916
def on_login_success(self, app_name, email):
917
"""Process the success of a login."""
918
logger.debug('UbuntuSSOWizardController.on_login_success')
919
result = yield self.login_success_callback(
920
unicode(app_name), unicode(email))
921
logger.debug('Result from callback is %s', result)
923
logger.info('Success in calling the given success_callback')
924
self.show_success_message()
926
logger.info('Error in calling the given success_callback')
927
self.show_error_message()
929
@defer.inlineCallbacks
930
def on_registration_success(self, app_name, email):
931
"""Process the success of a registration."""
932
logger.debug('UbuntuSSOWizardController.on_registration_success')
933
result = yield self.registration_success_callback(unicode(app_name),
935
logger.debug('Result from callback is %s', result)
937
logger.info('Success in calling the given registration_callback')
938
self.show_success_message()
940
logger.info('Success in calling the given registration_callback')
941
self.show_error_message()
943
def show_success_message(self):
944
"""Show the success message in the view."""
945
logger.info('Showing success message.')
946
# get the id of the success page, set it as the next id of the
947
# current page and let the wizard move to the next step
948
self.view.currentPage().next = self.view.success_page_id
950
# show the finish button but with a close message
952
buttons_layout.append(QWizard.Stretch)
953
buttons_layout.append(QWizard.FinishButton)
954
self.view.setButtonLayout(buttons_layout)
956
def show_error_message(self):
957
"""Show the error page in the view."""
958
logger.info('Showing error message.')
959
# similar to the success page but using the error id
960
self.view.currentPage().next = self.view.error_page_id
962
# show the finish button but with a close message
964
buttons_layout.append(QWizard.Stretch)
965
buttons_layout.append(QWizard.FinishButton)
966
self.view.setButtonLayout(buttons_layout)
968
#pylint: disable=C0103
969
def setupUi(self, view):
970
"""Setup the view."""
972
self.view.setWizardStyle(QWizard.ModernStyle)
973
self.view.button(QWizard.CancelButton).clicked.connect(
974
self.on_user_cancelation)
975
self.view.loginSuccess.connect(self.on_login_success)
976
self.view.registrationSuccess.connect(self.on_registration_success)
977
#pylint: enable=C0103