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

« back to all changes in this revision

Viewing changes to ubuntu_sso/qt/controllers.py

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

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright 2011-2012 Canonical Ltd.
4
 
#
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.
8
 
#
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.
13
 
#
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."""
17
 
 
18
 
import collections
19
 
import os
20
 
import StringIO
21
 
import tempfile
22
 
 
23
 
# pylint: disable=F0401
24
 
try:
25
 
    from PIL import Image
26
 
except ImportError:
27
 
    import Image
28
 
# pylint: enable=F0401
29
 
 
30
 
from PyQt4.QtGui import QMessageBox, QWizard, QPixmap
31
 
from twisted.internet import defer
32
 
 
33
 
from ubuntu_sso import main, NO_OP
34
 
from ubuntu_sso.logger import setup_logging
35
 
from ubuntu_sso.utils.ui import (
36
 
    CAPTCHA_LOAD_ERROR,
37
 
    CAPTCHA_REQUIRED_ERROR,
38
 
    CAPTCHA_SOLUTION_ENTRY,
39
 
    EMAIL1_ENTRY,
40
 
    EMAIL2_ENTRY,
41
 
    EMAIL_LABEL,
42
 
    EMAIL_INVALID,
43
 
    EMAIL_MISMATCH,
44
 
    ERROR,
45
 
    EXISTING_ACCOUNT_CHOICE_BUTTON,
46
 
    FORGOTTEN_PASSWORD_BUTTON,
47
 
    is_min_required_password,
48
 
    is_correct_email,
49
 
    JOIN_HEADER_LABEL,
50
 
    LOGIN_PASSWORD_LABEL,
51
 
    NAME_ENTRY,
52
 
    NAME_INVALID,
53
 
    PASSWORD1_ENTRY,
54
 
    PASSWORD2_ENTRY,
55
 
    PASSWORD_HELP,
56
 
    PASSWORD_MISMATCH,
57
 
    PASSWORD_TOO_WEAK,
58
 
    REQUEST_PASSWORD_TOKEN_LABEL,
59
 
    RESET_PASSWORD,
60
 
    REQUEST_PASSWORD_TOKEN_WRONG_EMAIL,
61
 
    REQUEST_PASSWORD_TOKEN_TECH_ERROR,
62
 
    SET_UP_ACCOUNT_CHOICE_BUTTON,
63
 
    SIGN_IN_BUTTON,
64
 
    TRY_AGAIN_BUTTON,
65
 
    VERIFY_EMAIL_TITLE,
66
 
    VERIFY_EMAIL_CONTENT,
67
 
)
68
 
 
69
 
 
70
 
ERROR_ALL = '__all__'
71
 
ERROR_EMAIL = 'email'
72
 
ERROR_EMAIL_TOKEN = 'email_token'
73
 
ERROR_MESSAGE = 'message'
74
 
ERROR_PASSWORD = 'password'
75
 
logger = setup_logging('ubuntu_sso.controllers')
76
 
 
77
 
 
78
 
# Based on the gtk implementation
79
 
def _build_general_error_message(errordict):
80
 
    """Build a user-friendly error message from the errordict."""
81
 
    result = ''
82
 
    if isinstance(errordict, collections.Mapping):
83
 
        msg1 = errordict.get(ERROR_ALL)
84
 
        msg2 = errordict.get(ERROR_MESSAGE)
85
 
        if msg2 is None:
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:
91
 
            result = msg1
92
 
        elif msg2 is not None:
93
 
            result = msg2
94
 
        else:
95
 
            if 'errtype' in errordict:
96
 
                del errordict['errtype']
97
 
            result = '\n'.join(
98
 
                [('%s: %s' % (k, v)) for k, v in errordict.iteritems()])
99
 
    else:
100
 
        result = repr(errordict)
101
 
    return result
102
 
 
103
 
 
104
 
class BackendController(object):
105
 
    """Represent a controller that talks with the sso self.backend."""
106
 
 
107
 
    def __init__(self, title='', subtitle=''):
108
 
        """Create a new instance."""
109
 
        self.view = None
110
 
        self.backend = None
111
 
        self._title = title
112
 
        self._subtitle = subtitle
113
 
 
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)
121
 
 
122
 
    #pylint: disable=C0103
123
 
    def pageInitialized(self):
124
 
        """Call to prepare the page just before it is shown."""
125
 
    #pylint: enable=C0103
126
 
 
127
 
 
128
 
class ChooseSignInController(BackendController):
129
 
    """Controlled to the ChooseSignIn view/widget."""
130
 
 
131
 
    def __init__(self, title='', subtitle=''):
132
 
        """Create a new instance to manage the view."""
133
 
        super(ChooseSignInController, self).__init__(title, subtitle)
134
 
        self.view = None
135
 
 
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."""
140
 
        self.view = view
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
146
 
 
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)
154
 
 
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)
161
 
 
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()
167
 
 
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()
173
 
 
174
 
 
175
 
class CurrentUserController(BackendController):
176
 
    """Controller used in the view that is used to allow the signin."""
177
 
 
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
184
 
 
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)
192
 
 
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)
205
 
 
206
 
    def _validate(self):
207
 
        """Perform input validation."""
208
 
        valid = True
209
 
        if not is_correct_email(unicode(self.view.ui.email_edit.text())) or \
210
 
            not unicode(self.view.ui.password_edit.text()):
211
 
            valid = False
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)
219
 
 
220
 
    def login(self):
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)
228
 
 
229
 
    def on_login_error(self, error):
230
 
        """There was an error when login in."""
231
 
        # let the user know
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'])
241
 
        else:
242
 
            self.message_box.critical(_build_general_error_message(error),
243
 
                self.view)
244
 
 
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.')
251
 
 
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()
259
 
 
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."""
265
 
        self.view = 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()
270
 
        self._connect_ui()
271
 
    #pylint: enable=C0103
272
 
 
273
 
 
274
 
class SetUpAccountController(BackendController):
275
 
    """Controller for the setup account view."""
276
 
 
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
283
 
 
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)
296
 
 
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,
301
 
                                                is_correct_email)
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)
318
 
 
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)
344
 
 
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())
353
 
 
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
360
 
 
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)
365
 
 
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?
370
 
        old_file = None
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')
374
 
        file_name = fd.name
375
 
        self.view.captcha_file = file_name
376
 
        d = self.backend.generate_captcha(self.view.wizard().app_name,
377
 
                                          file_name)
378
 
        if old_file:
379
 
            d.addCallback(lambda x: os.unlink(old_file))
380
 
        d.addErrback(self.on_captcha_generation_error)
381
 
        self.view.on_captcha_refreshing()
382
 
 
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)
389
 
 
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)
394
 
 
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()
417
 
 
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()
423
 
 
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)
430
 
        if msg:
431
 
            self.view.set_error_message(self.view.ui.email_assistance, msg)
432
 
        self.message_box.critical(_build_general_error_message(error),
433
 
            self.view)
434
 
 
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()
440
 
 
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())
450
 
        condition = True
451
 
        messages = []
452
 
        if not name:
453
 
            condition = False
454
 
            self.view.set_error_message(self.view.ui.name_assistance,
455
 
                NAME_INVALID)
456
 
        if not is_correct_email(email):
457
 
            condition = False
458
 
            self.view.set_error_message(self.view.ui.email_assistance,
459
 
                EMAIL_INVALID)
460
 
        if email != confirm_email:
461
 
            condition = False
462
 
            self.view.set_error_message(self.view.ui.confirm_email_assistance,
463
 
                EMAIL_MISMATCH)
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:
471
 
            condition = False
472
 
            self.message_box.critical('\n'.join(messages), self.view)
473
 
        return condition
474
 
 
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,
488
 
                                       captcha_solution)
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()
492
 
 
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
497
 
 
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
502
 
 
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
507
 
 
508
 
    #pylint: disable=C0103
509
 
    @defer.inlineCallbacks
510
 
    def setupUi(self, view):
511
 
        """Setup the view."""
512
 
        self.view = 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()
517
 
        self._set_titles()
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
524
 
 
525
 
 
526
 
class EmailVerificationController(BackendController):
527
 
    """Controller used for the verification page."""
528
 
 
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
535
 
 
536
 
    def _set_translated_strings(self):
537
 
        """Set the trnaslated strings."""
538
 
        logger.debug('EmailVerificationController._set_translated_strings')
539
 
 
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(
544
 
            self.validate_form)
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
550
 
 
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)
562
 
 
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(),
570
 
        })
571
 
 
572
 
    def set_titles(self):
573
 
        """This class needs to have a public set_titles.
574
 
 
575
 
        Since the subtitle contains data that is only known after SetupAccount
576
 
        and _set_titles is only called on initialization.
577
 
        """
578
 
        self._set_titles()
579
 
 
580
 
    #pylint: disable=C0103
581
 
    @defer.inlineCallbacks
582
 
    def setupUi(self, view):
583
 
        """Setup the view."""
584
 
        self.view = view
585
 
        self.backend = yield self.get_backend()
586
 
        self._set_titles()
587
 
        self._set_translated_strings()
588
 
        self._connect_ui_elements()
589
 
    #pylint: enable=C0103
590
 
 
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,
598
 
                                    password, code)
599
 
 
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)
605
 
 
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)
611
 
 
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
624
 
 
625
 
 
626
 
class ErrorController(BackendController):
627
 
    """Controller used for the error page."""
628
 
 
629
 
    def __init__(self, title='', subtitle=''):
630
 
        """Create a new instance."""
631
 
        super(ErrorController, self).__init__(title, subtitle)
632
 
        self.view = None
633
 
 
634
 
    #pylint: disable=C0103
635
 
    def setupUi(self, view):
636
 
        """Setup the view."""
637
 
        self.view = view
638
 
        self.view.next = -1
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
643
 
 
644
 
 
645
 
class ForgottenPasswordController(BackendController):
646
 
    """Controller used to deal with the forgotten pwd page."""
647
 
 
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)
655
 
 
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
670
 
 
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)
675
 
 
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)
684
 
 
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,
689
 
                                           is_correct_email)
690
 
 
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
703
 
 
704
 
    def _validate(self):
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)
714
 
 
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)
719
 
 
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()
725
 
 
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
732
 
            # again
733
 
            msg = REQUEST_PASSWORD_TOKEN_WRONG_EMAIL
734
 
        else:
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)
740
 
 
741
 
    #pylint: disable=C0103
742
 
    @defer.inlineCallbacks
743
 
    def setupUi(self, view):
744
 
        """Setup the view."""
745
 
        self.view = 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()
750
 
        self._connect_ui()
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
756
 
 
757
 
 
758
 
class ResetPasswordController(BackendController):
759
 
    """Controller used to deal with reseintg the password."""
760
 
 
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)
767
 
 
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
781
 
 
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)
786
 
 
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(
797
 
            self._validate)
798
 
 
799
 
    def _validate(self):
800
 
        """Enable the submit button if data is valid."""
801
 
        enabled = True
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):
807
 
            enabled = False
808
 
        elif not self.is_correct_password_confirmation(confirm_password):
809
 
            enabled = False
810
 
        elif not code:
811
 
            enabled = False
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)
819
 
 
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)
831
 
 
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:
841
 
                break
842
 
            self.view.wizard().back()
843
 
 
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),
849
 
            self.view)
850
 
 
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)
860
 
 
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
864
 
 
865
 
    #pylint: disable=C0103
866
 
    @defer.inlineCallbacks
867
 
    def setupUi(self, view):
868
 
        """Setup the view."""
869
 
        self.view = view
870
 
        self.backend = yield self.get_backend()
871
 
        self._set_translated_strings()
872
 
        self._connect_ui()
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
877
 
 
878
 
 
879
 
class SuccessController(BackendController):
880
 
    """Controller used for the success page."""
881
 
 
882
 
    def __init__(self, title='', subtitle=''):
883
 
        """Create a new instance."""
884
 
        super(SuccessController, self).__init__(title, subtitle)
885
 
        self.view = None
886
 
 
887
 
    #pylint: disable=C0103
888
 
    def setupUi(self, view):
889
 
        """Setup the view."""
890
 
        self.view = view
891
 
        self.view.next = -1
892
 
        self.view.header.set_title(self._title)
893
 
        self.view.header.set_subtitle(self._subtitle)
894
 
    #pylint: enable=C0103
895
 
 
896
 
 
897
 
class UbuntuSSOWizardController(object):
898
 
    """Controller used for the overall wizard."""
899
 
 
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."""
904
 
        self.view = None
905
 
        self.login_success_callback = login_success_callback
906
 
        self.registration_success_callback = registration_success_callback
907
 
        self.user_cancellation_callback = user_cancellation_callback
908
 
 
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)
913
 
        self.view.close()
914
 
 
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)
922
 
        if result == 0:
923
 
            logger.info('Success in calling the given success_callback')
924
 
            self.show_success_message()
925
 
        else:
926
 
            logger.info('Error in calling the given success_callback')
927
 
            self.show_error_message()
928
 
 
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),
934
 
                                                          unicode(email))
935
 
        logger.debug('Result from callback is %s', result)
936
 
        if result == 0:
937
 
            logger.info('Success in calling the given registration_callback')
938
 
            self.show_success_message()
939
 
        else:
940
 
            logger.info('Success in calling the given registration_callback')
941
 
            self.show_error_message()
942
 
 
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
949
 
        self.view.next()
950
 
        # show the finish button but with a close message
951
 
        buttons_layout = []
952
 
        buttons_layout.append(QWizard.Stretch)
953
 
        buttons_layout.append(QWizard.FinishButton)
954
 
        self.view.setButtonLayout(buttons_layout)
955
 
 
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
961
 
        self.view.next()
962
 
        # show the finish button but with a close message
963
 
        buttons_layout = []
964
 
        buttons_layout.append(QWizard.Stretch)
965
 
        buttons_layout.append(QWizard.FinishButton)
966
 
        self.view.setButtonLayout(buttons_layout)
967
 
 
968
 
    #pylint: disable=C0103
969
 
    def setupUi(self, view):
970
 
        """Setup the view."""
971
 
        self.view = 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