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

« back to all changes in this revision

Viewing changes to ubuntu_sso/qt/controllers.py

Fix pylint warnings

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
# with this program.  If not, see <http://www.gnu.org/licenses/>.
17
17
"""Controllers with the logic of the UI."""
18
18
 
 
19
import os
 
20
import tempfile
 
21
 
 
22
from PyQt4.QtGui import QMessageBox
19
23
from PyQt4.QtCore import QUrl
 
24
from twisted.internet.defer import inlineCallbacks, returnValue
20
25
 
21
26
from ubuntu_sso.logger import setup_logging
 
27
from ubuntu_sso.main.windows import UbuntuSSOClient
22
28
from ubuntu_sso.utils.ui import (
23
29
    CAPTCHA_SOLUTION_ENTRY,
24
30
    EMAIL1_ENTRY,
25
31
    EMAIL2_ENTRY,
 
32
    EMAIL_MISMATCH,
 
33
    EMAIL_INVALID,
26
34
    EXISTING_ACCOUNT_CHOICE_BUTTON,
27
35
    FORGOTTEN_PASSWORD_BUTTON,
28
36
    JOIN_HEADER_LABEL,
30
38
    PASSWORD1_ENTRY,
31
39
    PASSWORD2_ENTRY,
32
40
    PASSWORD_HELP,
 
41
    PASSWORD_MISMATCH,
 
42
    PASSWORD_TOO_WEAK,
33
43
    SET_UP_ACCOUNT_CHOICE_BUTTON,
34
44
    SET_UP_ACCOUNT_BUTTON,
35
45
    SIGN_IN_BUTTON,
 
46
    SUCCESS,
36
47
    SURNAME_ENTRY,
37
48
    TC_BUTTON,
38
49
    VERIFY_EMAIL_TITLE,
45
56
logger = setup_logging('ubuntu_sso.controllers')
46
57
 
47
58
 
 
59
class BackendController(object):
 
60
    """Represent a controller that talks with the sso backend."""
 
61
 
 
62
    def __init__(self):
 
63
        """Create a new instance."""
 
64
        self.root = None
 
65
        self.backends = []
 
66
 
 
67
    def __del__(self):
 
68
        """Clean the resources."""
 
69
        logger.info('Unregistering %s backends', len(self.backends))
 
70
        for backend in self.backends:
 
71
            backend.unregister_to_signals()
 
72
        if self.root is not None:
 
73
            logger.info('Disconnecting from root.')
 
74
            self.root.disconnect()
 
75
 
 
76
    @inlineCallbacks
 
77
    def get_backend(self):
 
78
        """Return the backend used by the controller."""
 
79
        # get the back end from the root
 
80
        if self.root is None:
 
81
            self.root = UbuntuSSOClient()
 
82
            self.root = yield self.root.connect()
 
83
        backend = self.root.sso_login
 
84
        yield backend.register_to_signals()
 
85
        self.backends.append(backend)
 
86
        returnValue(backend)
 
87
 
 
88
 
48
89
class ChooseSignInController(object):
49
90
    """Controlled to the ChooseSignIn view/widget."""
50
91
 
87
128
        view.wizard().next()
88
129
 
89
130
 
90
 
class CurrentUserController(object):
 
131
class CurrentUserController(BackendController):
91
132
    """Controller used in the view that is used to allow the signin."""
92
133
 
93
 
    def __init__(self, title=''):
 
134
    def __init__(self, backend=None, title='', app_name='', message_box=None):
94
135
        """Create a new instance."""
 
136
        super(CurrentUserController, self).__init__()
 
137
        if message_box is None:
 
138
            message_box = QMessageBox
 
139
        self.message_box = message_box
95
140
        self._title = title
 
141
        self._app_name = app_name
96
142
 
97
143
    def _set_translated_strings(self, view):
98
144
        """Set the translated strings."""
102
148
        view.forgot_password_label.setText(FORGOTTEN_PASSWORD_BUTTON)
103
149
        view.sign_in_button.setText(SIGN_IN_BUTTON)
104
150
 
 
151
    def _connect_buttons(self, view, backend):
 
152
        """Connect the buttons to perform actions."""
 
153
        view.sign_in_button.clicked.connect(lambda: self.login(view, backend))
 
154
        # lets add call backs to be execute for the calls we are interested
 
155
        backend.on_login_error_cb = lambda app, error:\
 
156
                                        self.on_login_error(view, app, error)
 
157
        backend.on_logged_in_cb = lambda app, result:\
 
158
                                        self.on_logged_in(view, app, result)
 
159
 
 
160
    def login(self, view, backend):
 
161
        """Perform the login using the backend."""
 
162
        logger.debug('Trying to log in using %s', backend)
 
163
        # grab the data from the view and call the backend
 
164
        d = backend.login(self._app_name, view.email, view.password)
 
165
        d.addErrback(lambda e: self.on_login_error(view, self._app_name, e))
 
166
 
 
167
    def on_login_error(self, view, app_name, error):
 
168
        """There was an error when login in."""
 
169
        # let the user know
 
170
        logger.error('Got error when login %s, error: %s', app_name, error)
 
171
        self.message_box.critical(view, app_name, error)
 
172
 
 
173
    def on_logged_in(self, view, app_name, result):
 
174
        """We managed to log in."""
 
175
        logger.info('Logged in for %s', app_name)
 
176
        self.message_box.information(view, app_name, SUCCESS)
 
177
        view.wizard.close()
 
178
 
105
179
    # use an ugly name just so have a simlar api as found in PyQt
106
180
    #pylint: disable=C0103
 
181
    @inlineCallbacks
107
182
    def setupUi(self, view):
108
183
        """Setup the view."""
 
184
        backend = yield self.get_backend()
109
185
        view.setTitle(self._title)
110
186
        self._set_translated_strings(view)
 
187
        self._connect_buttons(view, backend)
111
188
    #pylint: enable=C0103
112
189
 
113
190
 
114
 
class SetUpAccountController(object):
 
191
class SetUpAccountController(BackendController):
115
192
    """Conroller for the setup account view."""
116
193
 
117
194
    def __init__(self, tos_id=0, validation_id=1, app_name='',
118
 
                 help_message=''):
 
195
                 help_message='', message_box=None):
119
196
        """Create a new instance."""
 
197
        super(SetUpAccountController, self).__init__()
 
198
        if message_box is None:
 
199
            message_box = QMessageBox
 
200
        self.message_box = message_box
120
201
        self._tos_id = tos_id
121
202
        self._validation_id = validation_id
122
203
        self._app_name = app_name
157
238
        view.password_edit.textChanged.connect(
158
239
                                   view.confirm_password_edit.textChanged.emit)
159
240
 
160
 
    def _connect_ui_elements(self, view):
 
241
    def _connect_ui_elements(self, view, backend):
161
242
        """Set the connection of signals."""
162
243
        view.terms_and_conditions_button.clicked.connect(
163
244
                                               lambda: self.set_next_tos(view))
164
245
        view.set_up_button.clicked.connect(lambda: self.set_next_validation(
165
 
                                                                         view))
 
246
                                                                view, backend))
166
247
        view.password_edit.textChanged.connect(
167
248
                              lambda x: self.update_password_strength(x, view))
168
249
        view.terms_and_conditions_check_box.stateChanged.connect(
169
250
                                                 view.set_up_button.setEnabled)
 
251
        view.captcha_refresh_label.linkActivated.connect(lambda url:\
 
252
                                          self._refresh_captcha(view, backend))
 
253
        # set the callbacks for the captcha generation
 
254
        backend.on_captcha_generated_cb = lambda app, result:\
 
255
                                self.on_captcha_generated(view, app, result)
 
256
        backend.on_captcha_generation_error_cb = lambda app, error:\
 
257
                            self.on_captcha_generation_error(view, app, error)
 
258
        backend.on_user_registration_error_cb = lambda app, error:\
 
259
                            self.on_user_registration_error(view, app, error)
 
260
        return backend
 
261
 
 
262
    def _refresh_captcha(self, view, backend):
 
263
        """Refresh the captcha image shown in the ui."""
 
264
        # lets clean behind us, do we have the old file arround?
 
265
        old_file = None
 
266
        if view.captcha_file and os.path.exists(view.captcha_file):
 
267
            old_file = view.captcha_file
 
268
        fd = tempfile.TemporaryFile(mode='r')
 
269
        file_name = fd.name
 
270
        view.captcha_file = file_name
 
271
        d = backend.generate_captcha(self._app_name, file_name)
 
272
        if old_file:
 
273
            d.addCallback(lambda x: os.unlink(old_file))
 
274
        d.addErrback(lambda e: self.on_captcha_generation_error(view,
 
275
                                                                self._app_name,
 
276
                                                                e))
170
277
 
171
278
    def _set_titles(self, view):
172
279
        """Set the diff titles of the view."""
173
280
        view.setTitle(JOIN_HEADER_LABEL % {'app_name': self._app_name})
174
281
        view.setSubTitle(self._help_message)
175
282
 
 
283
    def on_captcha_generated(self, view, app_name, result):
 
284
        """A new image was generated."""
 
285
        view.captcha_image = result
 
286
 
 
287
    def on_captcha_generation_error(self, view, app_name, error):
 
288
        """An error ocurred."""
 
289
        self.message_box.critical(view, app_name, str(error))
 
290
 
 
291
    def on_user_registration_error(self, view, app_name, error):
 
292
        """Let the user know we could not register."""
 
293
        # error are returned as a dict with the data we want to show.
 
294
        self.message_box.critical(view, error['errtype'], error['email'])
 
295
 
176
296
    def set_next_tos(self, view):
177
297
        """Set the tos page as the next one."""
178
298
        view.next = self._tos_id
179
299
        view.wizard().next()
180
300
 
181
 
    def set_next_validation(self, view):
 
301
    def validate_form(self, view):
 
302
        """Validate the info of the form and return an error."""
 
303
        if not self.is_correct_email(view.email):
 
304
            self.message_box.critical(view, self._app_name, EMAIL_INVALID)
 
305
        if view.email_edit.text() != view.confirm_email_edit.text():
 
306
            self.message_box.critical(view, self._app_name, EMAIL_MISMATCH)
 
307
            return False
 
308
        if not is_min_required_password(str(view.password_edit.text())):
 
309
            self.message_box.critical(view, self._app_name, PASSWORD_TOO_WEAK)
 
310
            return False
 
311
        if view.password_edit.text() != view.confirm_password_edit.text():
 
312
            self.message_box.critical(view, self._app_name, PASSWORD_MISMATCH)
 
313
            return False
 
314
        return True
 
315
 
 
316
    def set_next_validation(self, view, backend):
182
317
        """Set the validation as the next page."""
183
 
        view.next = self._validation_id
184
 
        view.wizard().next()
 
318
        # validate the current info of the form, try to perform the action
 
319
        # to register the user, and then move foward
 
320
        if self.validate_form(view):
 
321
            backend.register_user(self._app_name, view.email, view.password,
 
322
                                  view.captcha_id, view.captcha_solution)
 
323
            view.next = self._validation_id
 
324
            view.wizard().next()
185
325
 
186
326
    def update_password_strength(self, password, view):
187
327
        """Callback used to update the password strenght UI code."""
203
343
        return view.password_edit.text() == password
204
344
 
205
345
    #pylint: disable=C0103
 
346
    @inlineCallbacks
206
347
    def setupUi(self, view):
207
348
        """Setup the view."""
 
349
        # request the backend to be used with the ui
 
350
        backend = yield self.get_backend()
 
351
        self._connect_ui_elements(view, backend)
 
352
        self._refresh_captcha(view, backend)
208
353
        self._set_titles(view)
209
354
        self._set_translated_strings(view)
210
355
        self._set_line_edits_validations(view)
211
 
        self._connect_ui_elements(view)
212
356
    #pylint: enable=C0103
213
357
 
214
358