~ubuntu-branches/ubuntu/natty/ubuntu-sso-client/natty

« back to all changes in this revision

Viewing changes to ubuntu_sso/gui.py

  • Committer: Daniel Holbach
  • Date: 2010-11-30 13:57:05 UTC
  • mfrom: (19.1.1 ubuntu-sso-client-1.1.5)
  • Revision ID: daniel.holbach@canonical.com-20101130135705-9iw0623qjcpuvpuq
Tags: 1.1.5-0ubuntu1
* New upstream release (1.1.5):
    * Use "org.freedesktop.secrets" dbus service instead of
    "org.gnome.keyring" (LP: #683088).
* New upstream release (1.1.4):
    * Added a gtk.Notebook to ensure proper window resize at startup
      (LP: #682669).
    * Enabled window resizing to be more user friendly.
    * Remove outdated references to gnome keyring from docstrings.
* New upstream release (1.1.3):
    * Make UI more friendly to resizes and big fonts (LP: #627496).
    * Splitting GUI code out of backend (LP: #677518).
    * Credentials can now be stored using a DBus called (LP: #680253).
    * Status from SSO server is now case sensitive (LP: #653165).
    * Credentials should not be cleared if the ping wasn't made due to empty
      ping url (LP: #676679).
   * Add the utils sub-package to the packages declaration so it installs
   (LP: #680593).
    * Fully async keyring access thru DBus. Drops dependency with
    gnomekeyring (LP: #656545).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# ubuntu_sso.gui - GUI for login and registration
4
 
#
5
 
# Author: Natalia Bidart <natalia.bidart@canonical.com>
6
 
#
7
 
# Copyright 2010 Canonical Ltd.
8
 
#
9
 
# This program is free software: you can redistribute it and/or modify it
10
 
# under the terms of the GNU General Public License version 3, as published
11
 
# by the Free Software Foundation.
12
 
#
13
 
# This program is distributed in the hope that it will be useful, but
14
 
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
 
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
 
# PURPOSE.  See the GNU General Public License for more details.
17
 
#
18
 
# You should have received a copy of the GNU General Public License along
19
 
# with this program.  If not, see <http://www.gnu.org/licenses/>.
20
 
 
21
 
"""User registration GUI."""
22
 
 
23
 
import logging
24
 
import os
25
 
import re
26
 
import tempfile
27
 
import webbrowser
28
 
 
29
 
from functools import wraps
30
 
 
31
 
import dbus
32
 
import gettext
33
 
import gobject
34
 
import gtk
35
 
import xdg
36
 
 
37
 
from dbus.mainloop.glib import DBusGMainLoop
38
 
 
39
 
from ubuntu_sso import DBUS_ACCOUNT_PATH, DBUS_BUS_NAME, DBUS_IFACE_USER_NAME
40
 
from ubuntu_sso.logger import setup_logging
41
 
 
42
 
 
43
 
# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
44
 
# pylint: disable=E1101
45
 
 
46
 
 
47
 
_ = gettext.gettext
48
 
gettext.textdomain('ubuntu-sso-client')
49
 
 
50
 
DBusGMainLoop(set_as_default=True)
51
 
logger = setup_logging('ubuntu_sso.gui')
52
 
 
53
 
# To be removed when Python bindings provide these constants
54
 
# as per http://code.google.com/p/pywebkitgtk/issues/detail?id=44
55
 
# WebKitLoadStatus
56
 
WEBKIT_LOAD_PROVISIONAL = 0
57
 
WEBKIT_LOAD_COMMITTED = 1
58
 
WEBKIT_LOAD_FINISHED = 2
59
 
WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT = 3
60
 
WEBKIT_LOAD_FAILED = 4
61
 
# WebKitWebNavigationReason
62
 
WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED = 0
63
 
WEBKIT_WEB_NAVIGATION_REASON_FORM_SUBMITTED = 1
64
 
WEBKIT_WEB_NAVIGATION_REASON_BACK_FORWARD = 2
65
 
WEBKIT_WEB_NAVIGATION_REASON_RELOAD = 3
66
 
WEBKIT_WEB_NAVIGATION_REASON_FORM_RESUBMITTED = 4
67
 
WEBKIT_WEB_NAVIGATION_REASON_OTHER = 5
68
 
 
69
 
 
70
 
NO_OP = lambda *args, **kwargs: None
71
 
DEFAULT_WIDTH = 30
72
 
# To be replaced by values from the theme (LP: #616526)
73
 
HELP_TEXT_COLOR = gtk.gdk.Color("#bfbfbf")
74
 
WARNING_TEXT_COLOR = gtk.gdk.Color("red")
75
 
 
76
 
SIG_LOGIN_FAILED = 'login-failed'
77
 
SIG_LOGIN_SUCCEEDED = 'login-succeeded'
78
 
SIG_REGISTRATION_FAILED = 'registration-failed'
79
 
SIG_REGISTRATION_SUCCEEDED = 'registration-succeeded'
80
 
SIG_USER_CANCELATION = 'user-cancelation'
81
 
 
82
 
SIGNAL_ARGUMENTS = [
83
 
    (SIG_LOGIN_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
84
 
    (SIG_LOGIN_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
85
 
    (SIG_REGISTRATION_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
86
 
    (SIG_REGISTRATION_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
87
 
    (SIG_USER_CANCELATION, (gobject.TYPE_STRING,)),
88
 
]
89
 
 
90
 
for sig, sig_args in SIGNAL_ARGUMENTS:
91
 
    gobject.signal_new(sig, gtk.Window, gobject.SIGNAL_RUN_FIRST,
92
 
                       gobject.TYPE_NONE, sig_args)
93
 
 
94
 
 
95
 
def get_data_dir():
96
 
    """Build absolute path to  the 'data' directory."""
97
 
    module = os.path.dirname(__file__)
98
 
    result = os.path.abspath(os.path.join(module, os.pardir, 'data'))
99
 
    logger.debug('get_data_file: trying to load from %r (exists? %s)',
100
 
                 result, os.path.exists(result))
101
 
    if os.path.exists(result):
102
 
        logger.info('get_data_file: returning data dir located at %r.', result)
103
 
        return result
104
 
 
105
 
    # no local data dir, looking within system data dirs
106
 
    data_dirs = xdg.BaseDirectory.xdg_data_dirs
107
 
    for path in data_dirs:
108
 
        result = os.path.join(path, 'ubuntu-sso-client', 'data')
109
 
        result = os.path.abspath(result)
110
 
        logger.debug('get_data_file: trying to load from %r (exists? %s)',
111
 
                     result, os.path.exists(result))
112
 
        if os.path.exists(result):
113
 
            logger.info('get_data_file: data dir located at %r.', result)
114
 
            return result
115
 
    else:
116
 
        msg = 'get_data_file: can not build a valid data path. Giving up.' \
117
 
              '__file__ is %r, data_dirs are %r'
118
 
        logger.error(msg, __file__, data_dirs)
119
 
 
120
 
 
121
 
def get_data_file(filename):
122
 
    """Build absolute path to 'filename' within the 'data' directory."""
123
 
    return os.path.join(get_data_dir(), filename)
124
 
 
125
 
 
126
 
def log_call(f):
127
 
    """Decorator to log call funtions."""
128
 
 
129
 
    @wraps(f)
130
 
    def inner(*args, **kwargs):
131
 
        """Execute 'f' logging the call as INFO."""
132
 
        logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
133
 
        return f(*args, **kwargs)
134
 
 
135
 
    return inner
136
 
 
137
 
 
138
 
class LabeledEntry(gtk.Entry):
139
 
    """An entry that displays the label within itself ina grey color."""
140
 
 
141
 
    def __init__(self, label, is_password=False, *args, **kwargs):
142
 
        self.label = label
143
 
        self.is_password = is_password
144
 
        self.warning = None
145
 
 
146
 
        super(LabeledEntry, self).__init__(*args, **kwargs)
147
 
 
148
 
        self.set_width_chars(DEFAULT_WIDTH)
149
 
        self._set_label(self, None)
150
 
        self.set_tooltip_text(self.label)
151
 
        self.connect('focus-in-event', self._clear_text)
152
 
        self.connect('focus-out-event', self._set_label)
153
 
        self.clear_warning()
154
 
        self.show()
155
 
 
156
 
    def _clear_text(self, *args, **kwargs):
157
 
        """Clear text and restore text color."""
158
 
        self.set_text(self.get_text())
159
 
 
160
 
        self.modify_text(gtk.STATE_NORMAL, None)  # restore to theme's default
161
 
 
162
 
        if self.is_password:
163
 
            self.set_visibility(False)
164
 
 
165
 
        return False  # propagate the event further
166
 
 
167
 
    def _set_label(self, *args, **kwargs):
168
 
        """Set the proper label and proper coloring."""
169
 
        if self.get_text():
170
 
            return
171
 
 
172
 
        self.set_text(self.label)
173
 
        self.modify_text(gtk.STATE_NORMAL, HELP_TEXT_COLOR)
174
 
 
175
 
        if self.is_password:
176
 
            self.set_visibility(True)
177
 
 
178
 
        return False  # propagate the event further
179
 
 
180
 
    def get_text(self):
181
 
        """Get text only if it's not the label nor empty."""
182
 
        result = super(LabeledEntry, self).get_text()
183
 
        if result == self.label or result.isspace():
184
 
            result = ''
185
 
        return result
186
 
 
187
 
    def set_warning(self, warning_msg):
188
 
        """Display warning as secondary icon, set 'warning_msg' as tooltip."""
189
 
        self.warning = warning_msg
190
 
        self.set_property('secondary-icon-stock', gtk.STOCK_DIALOG_WARNING)
191
 
        self.set_property('secondary-icon-sensitive', True)
192
 
        self.set_property('secondary-icon-activatable', False)
193
 
        self.set_property('secondary-icon-tooltip-text', warning_msg)
194
 
 
195
 
    def clear_warning(self):
196
 
        """Remove any warning."""
197
 
        self.warning = None
198
 
        self.set_property('secondary-icon-stock', None)
199
 
        self.set_property('secondary-icon-sensitive', False)
200
 
        self.set_property('secondary-icon-activatable', False)
201
 
        self.set_property('secondary-icon-tooltip-text', None)
202
 
 
203
 
 
204
 
class UbuntuSSOClientGUI(object):
205
 
    """Ubuntu single sign on GUI."""
206
 
 
207
 
    CAPTCHA_SOLUTION_ENTRY = _('Type the characters above')
208
 
    CAPTCHA_LOAD_ERROR = _('There was a problem getting the captcha, '
209
 
                           'reloading...')
210
 
    CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s ' \
211
 
                           'enter your details below.')
212
 
    EMAIL1_ENTRY = _('Email address')
213
 
    EMAIL2_ENTRY = _('Re-type Email address')
214
 
    EMAIL_MISMATCH = _('The email addresses don\'t match, please double check '
215
 
                       'and try entering them again.')
216
 
    EMAIL_INVALID = _('The email must be a valid email address.')
217
 
    EMAIL_TOKEN_ENTRY = _('Enter code verification here')
218
 
    ERROR = _('The process did not finish successfully.')
219
 
    FIELD_REQUIRED = _('This field is required.')
220
 
    FORGOTTEN_PASSWORD_BUTTON = _('I\'ve forgotten my password')
221
 
    JOIN_HEADER_LABEL = _('Create %(app_name)s account')
222
 
    LOADING = _('Loading...')
223
 
    LOGIN_BUTTON_LABEL = _('Already have an account? Click here to sign in')
224
 
    LOGIN_EMAIL_ENTRY = _('Email address')
225
 
    LOGIN_HEADER_LABEL = _('Connect to %(app_name)s')
226
 
    LOGIN_PASSWORD_ENTRY = _('Password')
227
 
    NAME_ENTRY = _('Name')
228
 
    NEXT = _('Next')
229
 
    ONE_MOMENT_PLEASE = _('One moment please...')
230
 
    PASSWORD_CHANGED = _('Your password was successfully changed.')
231
 
    PASSWORD1_ENTRY = RESET_PASSWORD1_ENTRY = _('Password')
232
 
    PASSWORD2_ENTRY = RESET_PASSWORD2_ENTRY = _('Re-type Password')
233
 
    PASSWORD_HELP = _('The password must have a minimum of 8 characters and ' \
234
 
                      'include one uppercase character and one number.')
235
 
    PASSWORD_MISMATCH = _('The passwords don\'t match, please double check ' \
236
 
                          'and try entering them again.')
237
 
    PASSWORD_TOO_WEAK = _('The password is too weak.')
238
 
    REQUEST_PASSWORD_TOKEN_LABEL = _('To reset your %(app_name)s password,'
239
 
                                     ' enter your email address below:')
240
 
    RESET_PASSWORD = _('Reset password')
241
 
    RESET_CODE_ENTRY = _('Reset code')
242
 
    RESET_EMAIL_ENTRY = _('Email address')
243
 
    SET_NEW_PASSWORD_LABEL = _('A password reset code has been sent to ' \
244
 
                               '%(email)s.\nPlease enter the code below ' \
245
 
                               'along with your new password.')
246
 
    SUCCESS = _('The process finished successfully. Congratulations!')
247
 
    TC_BUTTON = _('Show Terms & Conditions')
248
 
    TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \
249
 
                        'required to subscribe.')
250
 
    UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
251
 
                      'process. Please check the information and try again.')
252
 
    VERIFY_EMAIL_LABEL = ('<b>%s</b>\n\n' % _('Enter verification code') +
253
 
                          _('Check %(email)s for an email from'
254
 
                            ' Ubuntu Single Sign On.'
255
 
                            ' This message contains a verification code.'
256
 
                            ' Enter the code in the field below and click OK'
257
 
                            ' to complete creating your %(app_name)s account'))
258
 
    YES_TO_TC = _('I agree with the %(app_name)s terms and conditions')
259
 
    YES_TO_UPDATES = _('Yes! Email me %(app_name)s tips and updates.')
260
 
    CAPTCHA_RELOAD_TOOLTIP = _('Reload')
261
 
 
262
 
    def __init__(self, app_name, tc_url='', help_text='',
263
 
                 window_id=0, login_only=False, close_callback=None):
264
 
        """Create the GUI and initialize widgets."""
265
 
        gtk.link_button_set_uri_hook(NO_OP)
266
 
 
267
 
        self._captcha_filename = tempfile.mktemp()
268
 
        self._captcha_id = None
269
 
        self._signals_receivers = {}
270
 
        self._done = False  # whether the whole process was completed or not
271
 
 
272
 
        self.app_name = app_name
273
 
        self.app_label = '<b>%s</b>' % self.app_name
274
 
        self.tc_url = tc_url
275
 
        self.help_text = help_text
276
 
        self.close_callback = close_callback
277
 
        self.user_email = None
278
 
        self.user_password = None
279
 
 
280
 
        ui_filename = get_data_file('ui.glade')
281
 
        builder = gtk.Builder()
282
 
        builder.add_from_file(ui_filename)
283
 
        builder.connect_signals(self)
284
 
 
285
 
        self.widgets = []
286
 
        self.warnings = []
287
 
        self.cancels = []
288
 
        self.labels = []
289
 
        for obj in builder.get_objects():
290
 
            name = getattr(obj, 'name', None)
291
 
            if name is None and isinstance(obj, gtk.Buildable):
292
 
                # work around bug lp:507739
293
 
                name = gtk.Buildable.get_name(obj)
294
 
            if name is None:
295
 
                logging.warn("%s has no name (??)", obj)
296
 
            else:
297
 
                self.widgets.append(name)
298
 
                setattr(self, name, obj)
299
 
                if 'warning' in name:
300
 
                    self.warnings.append(obj)
301
 
                    obj.hide()
302
 
                if 'cancel_button' in name:
303
 
                    obj.connect('clicked', self.on_close_clicked)
304
 
                    self.cancels.append(obj)
305
 
                if 'label' in name:
306
 
                    self.labels.append(obj)
307
 
 
308
 
        self.entries = (u'name_entry', u'email1_entry', u'email2_entry',
309
 
                        u'password1_entry', u'password2_entry',
310
 
                        u'captcha_solution_entry', u'email_token_entry',
311
 
                        u'login_email_entry', u'login_password_entry',
312
 
                        u'reset_email_entry', u'reset_code_entry',
313
 
                        u'reset_password1_entry', u'reset_password2_entry')
314
 
 
315
 
        for name in self.entries:
316
 
            label = getattr(self, name.upper())
317
 
            is_password = 'password' in name
318
 
            entry = LabeledEntry(label=label, is_password=is_password)
319
 
            entry.set_activates_default(True)
320
 
            setattr(self, name, entry)
321
 
 
322
 
        self.window.set_icon_name('ubuntu-logo')
323
 
 
324
 
        self.bus = dbus.SessionBus()
325
 
        obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
326
 
                                  object_path=DBUS_ACCOUNT_PATH,
327
 
                                  follow_name_owner_changes=True)
328
 
        self.iface_name = DBUS_IFACE_USER_NAME
329
 
        self.backend = dbus.Interface(object=obj,
330
 
                                      dbus_interface=self.iface_name)
331
 
        logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
332
 
 
333
 
        self.pages = (self.enter_details_vbox, self.processing_vbox,
334
 
                      self.verify_email_vbox, self.finish_vbox,
335
 
                      self.tc_browser_vbox, self.login_vbox,
336
 
                      self.request_password_token_vbox,
337
 
                      self.set_new_password_vbox)
338
 
 
339
 
        self._append_page(self._build_processing_page())
340
 
        self._append_page(self._build_finish_page())
341
 
        self._append_page(self._build_login_page())
342
 
        self._append_page(self._build_request_password_token_page())
343
 
        self._append_page(self._build_set_new_password_page())
344
 
        self._append_page(self._build_verify_email_page())
345
 
 
346
 
        window_size = None
347
 
        if not login_only:
348
 
            window_size = (550, 500)
349
 
            self._append_page(self._build_enter_details_page())
350
 
            self._append_page(self._build_tc_page())
351
 
            self.login_button.grab_focus()
352
 
            self._set_current_page(self.enter_details_vbox)
353
 
        else:
354
 
            window_size = (400, 350)
355
 
            self.login_back_button.hide()
356
 
            self.login_ok_button.grab_focus()
357
 
            self.login_vbox.help_text = help_text
358
 
            self._set_current_page(self.login_vbox)
359
 
 
360
 
        self.window.set_size_request(*window_size)
361
 
        size_req = (int(window_size[0] * 0.9), -1)
362
 
        for label in self.labels:
363
 
            label.set_size_request(*size_req)
364
 
 
365
 
        self._signals = {
366
 
            'CaptchaGenerated':
367
 
             self._filter_by_app_name(self.on_captcha_generated),
368
 
            'CaptchaGenerationError':
369
 
             self._filter_by_app_name(self.on_captcha_generation_error),
370
 
            'UserRegistered':
371
 
             self._filter_by_app_name(self.on_user_registered),
372
 
            'UserRegistrationError':
373
 
             self._filter_by_app_name(self.on_user_registration_error),
374
 
            'EmailValidated':
375
 
             self._filter_by_app_name(self.on_email_validated),
376
 
            'EmailValidationError':
377
 
             self._filter_by_app_name(self.on_email_validation_error),
378
 
            'LoggedIn':
379
 
             self._filter_by_app_name(self.on_logged_in),
380
 
            'LoginError':
381
 
             self._filter_by_app_name(self.on_login_error),
382
 
            'UserNotValidated':
383
 
             self._filter_by_app_name(self.on_user_not_validated),
384
 
            'PasswordResetTokenSent':
385
 
             self._filter_by_app_name(self.on_password_reset_token_sent),
386
 
            'PasswordResetError':
387
 
             self._filter_by_app_name(self.on_password_reset_error),
388
 
            'PasswordChanged':
389
 
             self._filter_by_app_name(self.on_password_changed),
390
 
            'PasswordChangeError':
391
 
             self._filter_by_app_name(self.on_password_change_error),
392
 
        }
393
 
        self._setup_signals()
394
 
 
395
 
        if window_id != 0:
396
 
            # be as robust as possible:
397
 
            # if the window_id is not "good", set_transient_for will fail
398
 
            # awfully, and we don't want that: if the window_id is bad we can
399
 
            # still do everything as a standalone window. Also,
400
 
            # window_foreign_new may return None breaking set_transient_for.
401
 
            try:
402
 
                win = gtk.gdk.window_foreign_new(window_id)
403
 
                self.window.realize()
404
 
                self.window.window.set_transient_for(win)
405
 
            except:  # pylint: disable=W0702
406
 
                msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
407
 
                logger.exception(msg, window_id)
408
 
 
409
 
        # Hidding unused widgets to save some space (LP #627440).
410
 
        self.name_entry.hide()
411
 
        self.yes_to_updates_checkbutton.hide()
412
 
 
413
 
        self.window.show()
414
 
 
415
 
    @property
416
 
    def success_vbox(self):
417
 
        """The success page."""
418
 
        self.finish_vbox.label.set_markup('<span size="x-large">%s</span>' %
419
 
                                          self.SUCCESS)
420
 
        return self.finish_vbox
421
 
 
422
 
    @property
423
 
    def error_vbox(self):
424
 
        """The error page."""
425
 
        self.finish_vbox.label.set_markup('<span size="x-large">%s</span>' %
426
 
                                          self.ERROR)
427
 
        return self.finish_vbox
428
 
 
429
 
    # helpers
430
 
 
431
 
    def _filter_by_app_name(self, f):
432
 
        """Excecute the decorated function only for 'self.app_name'."""
433
 
 
434
 
        @wraps(f)
435
 
        def inner(app_name, *args, **kwargs):
436
 
            """Execute 'f' only if 'app_name' matches 'self.app_name'."""
437
 
            result = None
438
 
            if app_name == self.app_name:
439
 
                result = f(app_name, *args, **kwargs)
440
 
            else:
441
 
                logger.info('%s: ignoring call since received app_name '\
442
 
                            '"%s" (expected "%s")',
443
 
                            f.__name__, app_name, self.app_name)
444
 
            return result
445
 
 
446
 
        return inner
447
 
 
448
 
    def _setup_signals(self):
449
 
        """Bind signals to callbacks to be able to test the pages."""
450
 
        iface = self.iface_name
451
 
        for signal, method in self._signals.iteritems():
452
 
            actual = self._signals_receivers.get((iface, signal))
453
 
            if actual is not None:
454
 
                msg = 'Signal %r is already connected with %r at iface %r.'
455
 
                logger.warning(msg, signal, actual, iface)
456
 
 
457
 
            match = self.bus.add_signal_receiver(method, signal_name=signal,
458
 
                                                 dbus_interface=iface)
459
 
            logger.debug('Connecting signal %r with method %r at iface %r.' \
460
 
                         'Match: %r', signal, method, iface, match)
461
 
            self._signals_receivers[(iface, signal)] = method
462
 
 
463
 
    def _debug(self, *args, **kwargs):
464
 
        """Do some debugging."""
465
 
        print args, kwargs
466
 
 
467
 
    def _add_spinner_to_container(self, container, legend=None):
468
 
        """Add a spinner to 'container'."""
469
 
        spinner = gtk.Spinner()
470
 
        spinner.start()
471
 
 
472
 
        label = gtk.Label()
473
 
        if legend:
474
 
            label.set_text(legend)
475
 
        else:
476
 
            label.set_text(self.LOADING)
477
 
 
478
 
        hbox = gtk.HBox(spacing=5)
479
 
        hbox.pack_start(spinner, expand=False)
480
 
        hbox.pack_start(label, expand=False)
481
 
 
482
 
        alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
483
 
        alignment.add(hbox)
484
 
        alignment.show_all()
485
 
 
486
 
        # remove children to avoid:
487
 
        # GtkWarning: Attempting to add a widget with type GtkAlignment to a
488
 
        # GtkEventBox, but as a GtkBin subclass a GtkEventBox can only contain
489
 
        # one widget at a time
490
 
        for child in container.get_children():
491
 
            container.remove(child)
492
 
        container.add(alignment)
493
 
 
494
 
    def _set_warning_message(self, widget, message):
495
 
        """Set 'message' as text for 'widget'."""
496
 
        widget.set_text(message)
497
 
        widget.modify_fg(gtk.STATE_NORMAL, WARNING_TEXT_COLOR)
498
 
        widget.show()
499
 
 
500
 
    def _clear_warnings(self):
501
 
        """Clear all warning messages."""
502
 
        for widget in self.warnings:
503
 
            widget.set_text('')
504
 
            widget.hide()
505
 
        for widget in self.entries:
506
 
            getattr(self, widget).clear_warning()
507
 
 
508
 
    def _non_empty_input(self, widget):
509
 
        """Return weather widget has non empty content."""
510
 
        text = widget.get_text()
511
 
        return bool(text and not text.isspace())
512
 
 
513
 
    # build pages
514
 
 
515
 
    def _append_page(self, page):
516
 
        """Append 'page' to the 'window'."""
517
 
        self.window.get_children()[0].pack_start(page)
518
 
 
519
 
    def _set_header(self, header):
520
 
        """Set 'header' as the window title and header."""
521
 
        markup = '<span size="xx-large">%s</span>'
522
 
        self.header_label.set_markup(markup % header)
523
 
        self.window.set_title(self.header_label.get_text())  # avoid markup
524
 
 
525
 
    def _set_current_page(self, current_page, warning_text=None):
526
 
        """Hide all the pages but 'current_page'."""
527
 
        if hasattr(current_page, 'header'):
528
 
            self._set_header(current_page.header)
529
 
 
530
 
        if hasattr(current_page, 'help_text'):
531
 
            self.help_label.set_markup(current_page.help_text)
532
 
 
533
 
        if warning_text is not None:
534
 
            self._set_warning_message(self.warning_label, warning_text)
535
 
        else:
536
 
            self.warning_label.hide()
537
 
 
538
 
        for page in self.pages:
539
 
            if page is current_page:
540
 
                page.show()
541
 
            else:
542
 
                page.hide()
543
 
 
544
 
        if current_page.default_widget is not None:
545
 
            current_page.default_widget.grab_default()
546
 
 
547
 
    def _generate_captcha(self):
548
 
        """Ask for a new captcha; update the ui to reflect the fact."""
549
 
        logger.info('Calling generate_captcha with filename path at %r',
550
 
                    self._captcha_filename)
551
 
        self.backend.generate_captcha(self.app_name, self._captcha_filename,
552
 
                                      reply_handler=NO_OP, error_handler=NO_OP)
553
 
        self._set_captcha_loading()
554
 
 
555
 
    def _set_captcha_loading(self):
556
 
        """Present a spinner to the user while the captcha is downloaded."""
557
 
        self.captcha_image.hide()
558
 
        self._add_spinner_to_container(self.captcha_loading)
559
 
        white = gtk.gdk.Color('white')
560
 
        self.captcha_loading.modify_bg(gtk.STATE_NORMAL, white)
561
 
        self.captcha_loading.show_all()
562
 
        self.join_ok_button.set_sensitive(False)
563
 
 
564
 
    def _set_captcha_image(self):
565
 
        """Present a captcha image to the user to be resolved."""
566
 
        self.captcha_loading.hide()
567
 
        self.join_ok_button.set_sensitive(True)
568
 
        self.captcha_image.set_from_file(self._captcha_filename)
569
 
        self.captcha_image.show()
570
 
 
571
 
    def _build_enter_details_page(self):
572
 
        """Build the enter details page."""
573
 
        d = {'app_name': self.app_label}
574
 
        self.enter_details_vbox.header = self.JOIN_HEADER_LABEL % d
575
 
        self.enter_details_vbox.help_text = self.help_text
576
 
        self.enter_details_vbox.default_widget = self.join_ok_button
577
 
        self.join_ok_button.set_flags(gtk.CAN_DEFAULT)
578
 
 
579
 
        self.enter_details_vbox.pack_start(self.name_entry, expand=False)
580
 
        self.enter_details_vbox.reorder_child(self.name_entry, 0)
581
 
        entry = self.captcha_solution_entry
582
 
        self.captcha_solution_vbox.pack_start(entry, expand=False)
583
 
        msg = self.CAPTCHA_RELOAD_TOOLTIP
584
 
        self.captcha_reload_button.set_tooltip_text(msg)
585
 
 
586
 
        self.emails_hbox.pack_start(self.email1_entry, expand=False)
587
 
        self.emails_hbox.pack_start(self.email2_entry, expand=False)
588
 
 
589
 
        self.passwords_hbox.pack_start(self.password1_entry, expand=False)
590
 
        self.passwords_hbox.pack_start(self.password2_entry, expand=False)
591
 
        help_msg = '<small>%s</small>' % self.PASSWORD_HELP
592
 
        self.password_help_label.set_markup(help_msg)
593
 
 
594
 
        if not os.path.exists(self._captcha_filename):
595
 
            self._generate_captcha()
596
 
        else:
597
 
            self._set_captcha_image()
598
 
 
599
 
        msg = self.YES_TO_UPDATES % {'app_name': self.app_name}
600
 
        self.yes_to_updates_checkbutton.set_label(msg)
601
 
        if self.tc_url:
602
 
            msg = self.YES_TO_TC % {'app_name': self.app_name}
603
 
            self.yes_to_tc_checkbutton.set_label(msg)
604
 
            self.tc_button.set_label(self.TC_BUTTON)
605
 
        else:
606
 
            self.tc_vbox.hide_all()
607
 
        self.login_button.set_label(self.LOGIN_BUTTON_LABEL)
608
 
 
609
 
        return self.enter_details_vbox
610
 
 
611
 
    def _build_tc_page(self):
612
 
        """Build the Terms & Conditions page."""
613
 
        self.tc_browser_vbox.help_text = ''
614
 
        self.tc_browser_vbox.default_widget = self.tc_back_button
615
 
        self.tc_browser_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
616
 
        return self.tc_browser_vbox
617
 
 
618
 
    def _build_processing_page(self):
619
 
        """Build the processing page with a spinner."""
620
 
        self.processing_vbox.default_widget = None
621
 
        self._add_spinner_to_container(self.processing_vbox,
622
 
                                       legend=self.ONE_MOMENT_PLEASE)
623
 
        return self.processing_vbox
624
 
 
625
 
    def _build_verify_email_page(self):
626
 
        """Build the verify email page."""
627
 
        self.verify_email_vbox.default_widget = self.verify_token_button
628
 
        self.verify_email_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
629
 
 
630
 
        self.verify_email_details_vbox.pack_start(self.email_token_entry,
631
 
                                                  expand=False)
632
 
 
633
 
        return self.verify_email_vbox
634
 
 
635
 
    def _build_finish_page(self):
636
 
        """Build the success page."""
637
 
        self.finish_vbox.default_widget = self.finish_close_button
638
 
        self.finish_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
639
 
        self.finish_vbox.label = self.finish_label
640
 
        return self.finish_vbox
641
 
 
642
 
    def _build_login_page(self):
643
 
        """Build the login page."""
644
 
        d = {'app_name': self.app_label}
645
 
        self.login_vbox.header = self.LOGIN_HEADER_LABEL % d
646
 
        self.login_vbox.help_text = self.CONNECT_HELP_LABEL % d
647
 
        self.login_vbox.default_widget = self.login_ok_button
648
 
        self.login_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
649
 
 
650
 
        self.login_details_vbox.pack_start(self.login_email_entry)
651
 
        self.login_details_vbox.reorder_child(self.login_email_entry, 0)
652
 
        self.login_details_vbox.pack_start(self.login_password_entry)
653
 
        self.login_details_vbox.reorder_child(self.login_password_entry, 1)
654
 
 
655
 
        msg = self.FORGOTTEN_PASSWORD_BUTTON
656
 
        self.forgotten_password_button.set_label(msg)
657
 
        self.login_ok_button.grab_focus()
658
 
 
659
 
        return self.login_vbox
660
 
 
661
 
    def _build_request_password_token_page(self):
662
 
        """Build the login page."""
663
 
        self.request_password_token_vbox.header = self.RESET_PASSWORD
664
 
        text = self.REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': self.app_label}
665
 
        self.request_password_token_vbox.help_text = text
666
 
        btn = self.request_password_token_ok_button
667
 
        btn.set_flags(gtk.CAN_DEFAULT)
668
 
        self.request_password_token_vbox.default_widget = btn
669
 
 
670
 
        entry = self.reset_email_entry
671
 
        self.request_password_token_details_vbox.pack_start(entry,
672
 
                                                            expand=False)
673
 
        cb = self.on_reset_email_entry_changed
674
 
        self.reset_email_entry.connect('changed', cb)
675
 
        self.request_password_token_ok_button.set_label(self.NEXT)
676
 
        self.request_password_token_ok_button.set_sensitive(False)
677
 
 
678
 
        return self.request_password_token_vbox
679
 
 
680
 
    def _build_set_new_password_page(self):
681
 
        """Build the login page."""
682
 
        self.set_new_password_vbox.header = self.RESET_PASSWORD
683
 
        self.set_new_password_vbox.help_text = self.SET_NEW_PASSWORD_LABEL
684
 
        btn = self.set_new_password_ok_button
685
 
        btn.set_flags(gtk.CAN_DEFAULT)
686
 
        self.set_new_password_vbox.default_widget = btn
687
 
 
688
 
        for entry in (self.reset_code_entry,
689
 
                      self.reset_password1_entry,
690
 
                      self.reset_password2_entry):
691
 
            self.set_new_password_details_vbox.pack_start(entry, expand=False)
692
 
 
693
 
        cb = self.on_set_new_password_entries_changed
694
 
        self.reset_code_entry.connect('changed', cb)
695
 
        self.reset_password1_entry.connect('changed', cb)
696
 
        self.reset_password2_entry.connect('changed', cb)
697
 
        help_msg = '<small>%s</small>' % self.PASSWORD_HELP
698
 
        self.reset_password_help_label.set_markup(help_msg)
699
 
 
700
 
        self.set_new_password_ok_button.set_label(self.RESET_PASSWORD)
701
 
        self.set_new_password_ok_button.set_sensitive(False)
702
 
 
703
 
        return self.set_new_password_vbox
704
 
 
705
 
    def _validate_email(self, email1, email2=None):
706
 
        """Validate 'email1', return error message if not valid.
707
 
 
708
 
        If 'email2' is given, must match 'email1'.
709
 
        """
710
 
        if email2 is not None and email1 != email2:
711
 
            return self.EMAIL_MISMATCH
712
 
 
713
 
        if not email1:
714
 
            return self.FIELD_REQUIRED
715
 
 
716
 
        if '@' not in email1:
717
 
            return self.EMAIL_INVALID
718
 
 
719
 
    def _validate_password(self, password1, password2=None):
720
 
        """Validate 'password1', return error message if not valid.
721
 
 
722
 
        If 'password2' is given, must match 'email1'.
723
 
        """
724
 
        if password2 is not None and password1 != password2:
725
 
            return self.PASSWORD_MISMATCH
726
 
 
727
 
        if (len(password1) < 8 or
728
 
            re.search('[A-Z]', password1) is None or
729
 
            re.search('\d+', password1) is None):
730
 
            return self.PASSWORD_TOO_WEAK
731
 
 
732
 
    # GTK callbacks
733
 
 
734
 
    def run(self):
735
 
        """Run the application."""
736
 
        gtk.main()
737
 
 
738
 
    def connect(self, signal_name, handler, *args, **kwargs):
739
 
        """Connect 'signal_name' with 'handler'."""
740
 
        logger.debug('connect: signal %r, handler %r, args  %r, kwargs, %r',
741
 
                     signal_name, handler, args, kwargs)
742
 
        self.window.connect(signal_name, handler, *args, **kwargs)
743
 
 
744
 
    def emit(self, *args, **kwargs):
745
 
        """Emit a signal proxing the main window."""
746
 
        logger.debug('emit: args  %r, kwargs, %r', args, kwargs)
747
 
        self.window.emit(*args, **kwargs)
748
 
 
749
 
    def finish_success(self):
750
 
        """The whole process was completed succesfully. Show success page."""
751
 
        self._done = True
752
 
        self._set_current_page(self.success_vbox)
753
 
 
754
 
    def finish_error(self, error):
755
 
        """The whole process was not completed succesfully. Show error page."""
756
 
        self._done = True
757
 
        self._set_current_page(self.error_vbox)
758
 
 
759
 
    def on_close_clicked(self, *args, **kwargs):
760
 
        """Call self.close_callback if defined."""
761
 
        if os.path.exists(self._captcha_filename):
762
 
            os.remove(self._captcha_filename)
763
 
 
764
 
        # remove the signals from DBus
765
 
        remove = self.bus.remove_signal_receiver
766
 
        for (iface, signal) in self._signals_receivers.keys():
767
 
            method = self._signals_receivers.pop((iface, signal))
768
 
            logger.debug('Removing signal %r with method %r at iface %r.',
769
 
                         signal, method, iface)
770
 
            remove(method, signal_name=signal, dbus_interface=iface)
771
 
 
772
 
        # hide the main window
773
 
        if self.window is not None:
774
 
            self.window.hide()
775
 
 
776
 
        # process any pending events before emitting signals
777
 
        while gtk.events_pending():
778
 
            gtk.main_iteration()
779
 
 
780
 
        if not self._done:
781
 
            self.emit(SIG_USER_CANCELATION, self.app_name)
782
 
 
783
 
        # call user defined callback
784
 
        if self.close_callback is not None:
785
 
            logger.info('Calling custom close_callback %r with params %r, %r',
786
 
                        self.close_callback, args, kwargs)
787
 
            self.close_callback(*args, **kwargs)
788
 
 
789
 
    def on_sign_in_button_clicked(self, *args, **kwargs):
790
 
        """User wants to sign in, present the Login page."""
791
 
        self._set_current_page(self.login_vbox)
792
 
 
793
 
    def on_join_ok_button_clicked(self, *args, **kwargs):
794
 
        """Submit info for processing, present the processing vbox."""
795
 
        if not self.join_ok_button.is_sensitive():
796
 
            return
797
 
 
798
 
        self._clear_warnings()
799
 
 
800
 
        error = False
801
 
 
802
 
        # Hidding unused widgets to save some space (LP #627440).
803
 
        #name = self.name_entry.get_text()
804
 
        #if not name:
805
 
        #    self.name_entry.set_warning(self.FIELD_REQUIRED)
806
 
        #    error = True
807
 
 
808
 
        # check email
809
 
        email1 = self.email1_entry.get_text()
810
 
        email2 = self.email2_entry.get_text()
811
 
        msg = self._validate_email(email1, email2)
812
 
        if msg is not None:
813
 
            self.email1_entry.set_warning(msg)
814
 
            self.email2_entry.set_warning(msg)
815
 
            error = True
816
 
 
817
 
        # check password
818
 
        password1 = self.password1_entry.get_text()
819
 
        password2 = self.password2_entry.get_text()
820
 
        msg = self._validate_password(password1, password2)
821
 
        if msg is not None:
822
 
            self.password1_entry.set_warning(msg)
823
 
            self.password2_entry.set_warning(msg)
824
 
            error = True
825
 
 
826
 
        # check T&C
827
 
        if not self.yes_to_tc_checkbutton.get_active():
828
 
            self._set_warning_message(self.tc_warning_label,
829
 
                                      self.TC_NOT_ACCEPTED)
830
 
            error = True
831
 
 
832
 
        captcha_solution = self.captcha_solution_entry.get_text()
833
 
        if not captcha_solution:
834
 
            self.captcha_solution_entry.set_warning(self.FIELD_REQUIRED)
835
 
            error = True
836
 
 
837
 
        if error:
838
 
            return
839
 
 
840
 
        self._set_current_page(self.processing_vbox)
841
 
        self.user_email = email1
842
 
        self.user_password = password1
843
 
 
844
 
        logger.info('Calling register_user with email %r, password <hidden>,' \
845
 
                    ' captcha_id %r and captcha_solution %r.', email1,
846
 
                    self._captcha_id, captcha_solution)
847
 
        f = self.backend.register_user
848
 
        f(self.app_name, email1, password1, self._captcha_id, captcha_solution,
849
 
          reply_handler=NO_OP, error_handler=NO_OP)
850
 
 
851
 
    def on_verify_token_button_clicked(self, *args, **kwargs):
852
 
        """The user entered the email token, let's verify!"""
853
 
        if not self.verify_token_button.is_sensitive():
854
 
            return
855
 
 
856
 
        self._clear_warnings()
857
 
 
858
 
        email_token = self.email_token_entry.get_text()
859
 
        if not email_token:
860
 
            self.email_token_entry.set_warning(self.FIELD_REQUIRED)
861
 
            return
862
 
 
863
 
        email = self.user_email
864
 
        password = self.user_password
865
 
        f = self.backend.validate_email
866
 
        logger.info('Calling validate_email with email %r, password <hidden>' \
867
 
                    ', app_name %r and email_token %r.', email, self.app_name,
868
 
                    email_token)
869
 
        f(self.app_name, email, password, email_token,
870
 
          reply_handler=NO_OP, error_handler=NO_OP)
871
 
 
872
 
        self._set_current_page(self.processing_vbox)
873
 
 
874
 
    def on_login_connect_button_clicked(self, *args, **kwargs):
875
 
        """User wants to connect!"""
876
 
        if not self.login_ok_button.is_sensitive():
877
 
            return
878
 
 
879
 
        self._clear_warnings()
880
 
 
881
 
        error = False
882
 
 
883
 
        email = self.login_email_entry.get_text()
884
 
        msg = self._validate_email(email)
885
 
        if msg is not None:
886
 
            self.login_email_entry.set_warning(msg)
887
 
            error = True
888
 
 
889
 
        password = self.login_password_entry.get_text()
890
 
        if not password:
891
 
            self.login_password_entry.set_warning(self.FIELD_REQUIRED)
892
 
            error = True
893
 
 
894
 
        if error:
895
 
            return
896
 
 
897
 
        f = self.backend.login
898
 
        f(self.app_name, email, password,
899
 
          reply_handler=NO_OP, error_handler=NO_OP)
900
 
 
901
 
        self._set_current_page(self.processing_vbox)
902
 
        self.user_email = email
903
 
        self.user_password = password
904
 
 
905
 
    def on_login_back_button_clicked(self, *args, **kwargs):
906
 
        """User wants to go to the previous page."""
907
 
        self._set_current_page(self.enter_details_vbox)
908
 
 
909
 
    def on_forgotten_password_button_clicked(self, *args, **kwargs):
910
 
        """User wants to reset the password."""
911
 
        self._set_current_page(self.request_password_token_vbox)
912
 
 
913
 
    def on_request_password_token_ok_button_clicked(self, *args, **kwargs):
914
 
        """User entered the email address to reset the password."""
915
 
        if not self.request_password_token_ok_button.is_sensitive():
916
 
            return
917
 
 
918
 
        self._clear_warnings()
919
 
 
920
 
        email = self.reset_email_entry.get_text()
921
 
        msg = self._validate_email(email)
922
 
        if msg is not None:
923
 
            self.reset_email_entry.set_warning(msg)
924
 
            return
925
 
 
926
 
        logger.info('Calling request_password_reset_token with %r.', email)
927
 
        f = self.backend.request_password_reset_token
928
 
        f(self.app_name, email, reply_handler=NO_OP, error_handler=NO_OP)
929
 
 
930
 
        self._set_current_page(self.processing_vbox)
931
 
 
932
 
    def on_request_password_token_back_button_clicked(self, *args, **kwargs):
933
 
        """User wants to go to the previous page."""
934
 
        self._set_current_page(self.login_vbox)
935
 
 
936
 
    def on_reset_email_entry_changed(self, widget, *args, **kwargs):
937
 
        """User is changing the 'widget' entry in the reset email page."""
938
 
        sensitive = self._non_empty_input(widget)
939
 
        self.request_password_token_ok_button.set_sensitive(sensitive)
940
 
 
941
 
    def on_set_new_password_entries_changed(self, *args, **kwargs):
942
 
        """User is changing the 'widget' entry in the reset password page."""
943
 
        sensitive = True
944
 
        for entry in (self.reset_code_entry,
945
 
                      self.reset_password1_entry,
946
 
                      self.reset_password2_entry):
947
 
            sensitive &= self._non_empty_input(entry)
948
 
        self.set_new_password_ok_button.set_sensitive(sensitive)
949
 
 
950
 
    def on_set_new_password_ok_button_clicked(self, *args, **kwargs):
951
 
        """User entered reset code and new passwords."""
952
 
        if not self.set_new_password_ok_button.is_sensitive():
953
 
            return
954
 
 
955
 
        self._clear_warnings()
956
 
 
957
 
        error = False
958
 
 
959
 
        token = self.reset_code_entry.get_text()
960
 
        if not token:
961
 
            self.reset_code_entry.set_warning(self.FIELD_REQUIRED)
962
 
            error = True
963
 
 
964
 
        password1 = self.reset_password1_entry.get_text()
965
 
        password2 = self.reset_password2_entry.get_text()
966
 
        msg = self._validate_password(password1, password2)
967
 
        if msg is not None:
968
 
            self.reset_password1_entry.set_warning(msg)
969
 
            self.reset_password2_entry.set_warning(msg)
970
 
            error = True
971
 
 
972
 
        if error:
973
 
            return
974
 
 
975
 
        email = self.reset_email_entry.get_text()
976
 
        logger.info('Calling set_new_password with email %r, token %r and ' \
977
 
                    'new password: <hidden>.', email, token)
978
 
        f = self.backend.set_new_password
979
 
        f(self.app_name, email, token, password1,
980
 
          reply_handler=NO_OP, error_handler=NO_OP)
981
 
 
982
 
        self._set_current_page(self.processing_vbox)
983
 
 
984
 
    def on_tc_button_clicked(self, *args, **kwargs):
985
 
        """The T&C button was clicked, create the browser and load terms."""
986
 
        # delay the import of webkit to be able to build without it
987
 
        import webkit
988
 
        browser = webkit.WebView()
989
 
 
990
 
        # The signal WebKitWebView::load-finished is deprecated and should not
991
 
        # be used in newly-written code. Use the "load-status" property
992
 
        # instead. Connect to "notify::load-status" to monitor loading.
993
 
 
994
 
        # nataliabidart (2010-10-04): connecting this signal makes the loading
995
 
        # of the Ubuntu One terms URL to fail. So we're using the deprecated
996
 
        # 'load-finished' for now.
997
 
 
998
 
        #browser.connect('notify::load-status',
999
 
        #                self.on_tc_browser_notify_load_status)
1000
 
        browser.connect('load-finished',
1001
 
                        self.on_tc_browser_notify_load_status)
1002
 
        browser.connect('navigation-policy-decision-requested',
1003
 
                        self.on_tc_browser_navigation_requested)
1004
 
 
1005
 
        settings = browser.get_settings()
1006
 
        settings.set_property("enable-plugins", False)
1007
 
        settings.set_property("enable-default-context-menu", False)
1008
 
 
1009
 
        # webkit_web_view_open has been deprecated since version 1.1.1 and
1010
 
        # should not be used in newly-written code. Use
1011
 
        # webkit_web_view_load_uri() instead.
1012
 
        browser.load_uri(self.tc_url)
1013
 
        browser.show()
1014
 
        self.tc_browser_window.add(browser)
1015
 
        self._set_current_page(self.processing_vbox)
1016
 
 
1017
 
    def on_tc_back_button_clicked(self, *args, **kwargs):
1018
 
        """T & C 'back' button was clicked, return to the previous page."""
1019
 
        self._set_current_page(self.enter_details_vbox)
1020
 
 
1021
 
    def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
1022
 
        """The T&C page is being loaded."""
1023
 
        if browser.get_load_status() == WEBKIT_LOAD_FINISHED:
1024
 
            self._set_current_page(self.tc_browser_vbox)
1025
 
 
1026
 
    def on_tc_browser_navigation_requested(self, browser, frame, request,
1027
 
                                           action, decision, *args, **kwargs):
1028
 
        """The user wants to navigate within the T&C browser."""
1029
 
        if action is not None and \
1030
 
           action.get_reason() == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED:
1031
 
            if decision is not None:
1032
 
                decision.ignore()
1033
 
            url = action.get_original_uri()
1034
 
            webbrowser.open(url)
1035
 
        else:
1036
 
            if decision is not None:
1037
 
                decision.use()
1038
 
 
1039
 
    def on_tc_browser_vbox_hide(self, *args, **kwargs):
1040
 
        """The T&C page is no longer being shown."""
1041
 
        children = self.tc_browser_window.get_children()
1042
 
        if len(children) > 0:
1043
 
            browser = children[0]
1044
 
            self.tc_browser_window.remove(browser)
1045
 
            browser.destroy()
1046
 
            del browser
1047
 
 
1048
 
    def on_captcha_reload_button_clicked(self, *args, **kwargs):
1049
 
        """User clicked the reload captcha button."""
1050
 
        self._generate_captcha()
1051
 
 
1052
 
    # backend callbacks
1053
 
 
1054
 
    def _build_general_error_message(self, errordict):
1055
 
        """Concatenate __all__ and message from the errordict."""
1056
 
        result = None
1057
 
        msg1 = errordict.get('__all__')
1058
 
        msg2 = errordict.get('message')
1059
 
        if msg1 is not None and msg2 is not None:
1060
 
            result = '\n'.join((msg1, msg2))
1061
 
        else:
1062
 
            result = msg1 if msg1 is not None else msg2
1063
 
        return result
1064
 
 
1065
 
    @log_call
1066
 
    def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
1067
 
        """Captcha image has been generated and is available to be shown."""
1068
 
        if captcha_id is None:
1069
 
            logger.warning('on_captcha_generated: captcha_id is None for '
1070
 
                           'app_name "%s".', app_name)
1071
 
        self._captcha_id = captcha_id
1072
 
        self._set_captcha_image()
1073
 
 
1074
 
    @log_call
1075
 
    def on_captcha_generation_error(self, app_name, error, *args, **kwargs):
1076
 
        """Captcha image generation failed."""
1077
 
        self._set_warning_message(self.warning_label, self.CAPTCHA_LOAD_ERROR)
1078
 
        self._generate_captcha()
1079
 
 
1080
 
    @log_call
1081
 
    def on_user_registered(self, app_name, email, *args, **kwargs):
1082
 
        """Registration can go on, user needs to verify email."""
1083
 
        help_text = self.VERIFY_EMAIL_LABEL % {'app_name': self.app_name,
1084
 
                                               'email': email}
1085
 
        self.verify_email_vbox.help_text = help_text
1086
 
        self._set_current_page(self.verify_email_vbox)
1087
 
 
1088
 
    @log_call
1089
 
    def on_user_registration_error(self, app_name, error, *args, **kwargs):
1090
 
        """Captcha image generation failed."""
1091
 
        msg = error.get('email')
1092
 
        if msg is not None:
1093
 
            self.email1_entry.set_warning(msg)
1094
 
            self.email2_entry.set_warning(msg)
1095
 
 
1096
 
        msg = error.get('password')
1097
 
        if msg is not None:
1098
 
            self.password1_entry.set_warning(msg)
1099
 
            self.password2_entry.set_warning(msg)
1100
 
 
1101
 
        msg = self._build_general_error_message(error)
1102
 
        self._generate_captcha()
1103
 
        self._set_current_page(self.enter_details_vbox, warning_text=msg)
1104
 
 
1105
 
    @log_call
1106
 
    def on_email_validated(self, app_name, email, *args, **kwargs):
1107
 
        """User email was successfully verified."""
1108
 
        self._done = True
1109
 
        self.emit(SIG_REGISTRATION_SUCCEEDED, self.app_name, email)
1110
 
 
1111
 
    @log_call
1112
 
    def on_email_validation_error(self, app_name, error, *args, **kwargs):
1113
 
        """User email validation failed."""
1114
 
        msg = error.get('email_token')
1115
 
        if msg is not None:
1116
 
            self.email_token_entry.set_warning(msg)
1117
 
 
1118
 
        msg = self._build_general_error_message(error)
1119
 
        self._set_current_page(self.verify_email_vbox, warning_text=msg)
1120
 
 
1121
 
    @log_call
1122
 
    def on_logged_in(self, app_name, email, *args, **kwargs):
1123
 
        """User was successfully logged in."""
1124
 
        self._done = True
1125
 
        self.emit(SIG_LOGIN_SUCCEEDED, self.app_name, email)
1126
 
 
1127
 
    @log_call
1128
 
    def on_login_error(self, app_name, error, *args, **kwargs):
1129
 
        """User was not successfully logged in."""
1130
 
        msg = self._build_general_error_message(error)
1131
 
        self._set_current_page(self.login_vbox, warning_text=msg)
1132
 
 
1133
 
    @log_call
1134
 
    def on_user_not_validated(self, app_name, email, *args, **kwargs):
1135
 
        """User was not validated."""
1136
 
        self.on_user_registered(app_name, email)
1137
 
 
1138
 
    @log_call
1139
 
    def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
1140
 
        """Password reset token was successfully sent."""
1141
 
        msg = self.SET_NEW_PASSWORD_LABEL % {'email': email}
1142
 
        self.set_new_password_vbox.help_text = msg
1143
 
        self._set_current_page(self.set_new_password_vbox)
1144
 
 
1145
 
    @log_call
1146
 
    def on_password_reset_error(self, app_name, error, *args, **kwargs):
1147
 
        """Password reset failed."""
1148
 
        msg = self._build_general_error_message(error)
1149
 
        self._set_current_page(self.login_vbox, warning_text=msg)
1150
 
 
1151
 
    @log_call
1152
 
    def on_password_changed(self, app_name, email, *args, **kwargs):
1153
 
        """Password was successfully changed."""
1154
 
        self._set_current_page(self.login_vbox,
1155
 
                               warning_text=self.PASSWORD_CHANGED)
1156
 
 
1157
 
    @log_call
1158
 
    def on_password_change_error(self, app_name, error, *args, **kwargs):
1159
 
        """Password reset failed."""
1160
 
        msg = self._build_general_error_message(error)
1161
 
        self._set_current_page(self.request_password_token_vbox,
1162
 
                               warning_text=msg)