Merge lp:~mandel/ubuntu-sso-client/windows_ui_4 into lp:ubuntu-sso-client
- windows_ui_4
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Manuel de la Peña |
Approved revision: | 757 |
Merged at revision: | 698 |
Proposed branch: | lp:~mandel/ubuntu-sso-client/windows_ui_4 |
Merge into: | lp:ubuntu-sso-client |
Prerequisite: | lp:~mandel/ubuntu-sso-client/windows_ui_3 |
Diff against target: |
910 lines (+389/-89) 9 files modified
data/qt/setup_account.ui (+6/-0) pylintrc (+3/-2) setup.py (+6/-0) ubuntu_sso/main/tests/test_windows.py (+45/-26) ubuntu_sso/main/windows.py (+18/-14) ubuntu_sso/qt/controllers.py (+154/-10) ubuntu_sso/qt/gui.py (+18/-29) ubuntu_sso/qt/tests/show_gui.py (+33/-0) ubuntu_sso/qt/tests/test_windows.py (+106/-8) |
To merge this branch: | bzr merge lp:~mandel/ubuntu-sso-client/windows_ui_4 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Shane Fagan (community) | Approve | ||
Roberto Alsina (community) | Approve | ||
Review via email: mp+56308@code.launchpad.net |
Commit message
Partial fix for lp:732203
Add SSO functionality to the Qt UI allowing to register and sign in users using the Ubuntu SSO client backend.
Description of the change
Partial fix for lp:732203
This branch adds SSO functionality to the Qt UI allowing to register and sign in users using the Ubuntu SSO client backend.
Shane Fagan (shanepatrickfagan) : | # |
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
The attempt to merge lp:~mandel/ubuntu-sso-client/windows_ui_4 into lp:ubuntu-sso-client failed. Below is the output from the failed tests.
Running test suite for ubuntu_sso
Xlib: extension "RANDR" missing on display ":99".
ubuntu_
AccountTestCase
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
EnvironOverri
test_
test_
twisted.
TestCase
runTest ... [OK]
ubuntu_
BasicTestCase
runTest ... [OK]
ClearCredenti
test_
test_
CredentialsAu
test_
CredentialsCa
test_
test_
test_
test_error_cb ... [OK]
test_
t...
Preview Diff
1 | === modified file 'data/qt/setup_account.ui' |
2 | --- data/qt/setup_account.ui 2011-03-30 14:28:05 +0000 |
3 | +++ data/qt/setup_account.ui 2011-04-12 15:24:35 +0000 |
4 | @@ -290,6 +290,12 @@ |
5 | </property> |
6 | <item> |
7 | <widget class="QLabel" name="captcha_view"> |
8 | + <property name="minimumSize"> |
9 | + <size> |
10 | + <width>0</width> |
11 | + <height>57</height> |
12 | + </size> |
13 | + </property> |
14 | <property name="frameShape"> |
15 | <enum>QFrame::Box</enum> |
16 | </property> |
17 | |
18 | === modified file 'pylintrc' |
19 | --- pylintrc 2010-09-22 16:14:28 +0000 |
20 | +++ pylintrc 2011-04-12 15:24:35 +0000 |
21 | @@ -20,7 +20,7 @@ |
22 | |
23 | # Add <file or directory> to the black list. It should be a base name, not a |
24 | # path. You may set this option multiple times. |
25 | -#ignore=<somedir> |
26 | +#ignore=qt |
27 | |
28 | # Pickle collected data for later comparisons. |
29 | persistent=no |
30 | @@ -50,7 +50,8 @@ |
31 | # W0142: Used * or ** magic |
32 | # W0613: Unused argument 'yyy' |
33 | # C0302: Too many lines in module |
34 | -disable=R,I,W0142,W0613,C0302 |
35 | +# W0404: Reimport of module |
36 | +disable=R,I,W0142,W0613,C0302,W0404 |
37 | |
38 | |
39 | [REPORTS] |
40 | |
41 | === modified file 'setup.py' |
42 | --- setup.py 2011-03-31 13:55:42 +0000 |
43 | +++ setup.py 2011-04-12 15:24:35 +0000 |
44 | @@ -49,6 +49,9 @@ |
45 | |
46 | description = 'build extra files needed by ubuntu-sso-client' |
47 | |
48 | + def __init__(self): |
49 | + build_extra.build_extra.__init__(self) |
50 | + |
51 | def run(self): |
52 | """Do the build.""" |
53 | sed = find_executable('sed') |
54 | @@ -93,6 +96,9 @@ |
55 | |
56 | description = "build PyQt GUIs (.ui) and resources (.qrc)" |
57 | |
58 | + def __init__(self): |
59 | + build_extra.build_extra.__init__(self) |
60 | + |
61 | def compile_ui(self, ui_file, py_file=None): |
62 | """Compile the .ui files to python modules.""" |
63 | # Search for pyuic4 in python bin dir, then in the $Path. |
64 | |
65 | === modified file 'ubuntu_sso/main/tests/test_windows.py' |
66 | --- ubuntu_sso/main/tests/test_windows.py 2011-03-30 18:50:04 +0000 |
67 | +++ ubuntu_sso/main/tests/test_windows.py 2011-04-12 15:24:35 +0000 |
68 | @@ -40,8 +40,9 @@ |
69 | SSOLoginClient, |
70 | UbuntuSSORoot) |
71 | |
72 | -# because we are using twisted we have java like names C0103 |
73 | -# pylint: disable=C0103 |
74 | +# because we are using twisted we have java like names C0103 and |
75 | +# the issues that mocker brings with it like W0104 |
76 | +# pylint: disable=C0103,W0104 |
77 | |
78 | |
79 | class SaveProtocolServerFactory(PBServerFactory): |
80 | @@ -141,14 +142,17 @@ |
81 | def test_emit_captcha_generation_error(self): |
82 | """Test that the cb was called.""" |
83 | app_name = 'app' |
84 | - raised_error = 'error' |
85 | + error_value = 'error' |
86 | + raised_error = self.mocker.mock() |
87 | |
88 | @defer.inlineCallbacks |
89 | def test_emit(client): |
90 | """Actual test.""" |
91 | - self.except_to_errdict(raised_error) |
92 | - self.mocker.result(raised_error) |
93 | - client.on_captcha_generation_error_cb(app_name, raised_error) |
94 | + raised_error.value |
95 | + self.mocker.result(error_value) |
96 | + self.except_to_errdict(error_value) |
97 | + self.mocker.result(error_value) |
98 | + client.on_captcha_generation_error_cb(app_name, error_value) |
99 | self.mocker.replay() |
100 | self.login.emit_captcha_generation_error(app_name, raised_error) |
101 | yield client.unregister_to_signals() |
102 | @@ -205,14 +209,17 @@ |
103 | def test_emit_user_registration_error(self): |
104 | """Test that the cb was called.""" |
105 | app_name = 'app' |
106 | - raised_error = 'error' |
107 | + error_value = 'error' |
108 | + raised_error = self.mocker.mock() |
109 | |
110 | @defer.inlineCallbacks |
111 | def test_emit(client): |
112 | """Actual test.""" |
113 | - self.except_to_errdict(raised_error) |
114 | - self.mocker.result(raised_error) |
115 | - client.on_user_registration_error_cb(app_name, raised_error) |
116 | + raised_error.value |
117 | + self.mocker.result(error_value) |
118 | + self.except_to_errdict(error_value) |
119 | + self.mocker.result(error_value) |
120 | + client.on_user_registration_error_cb(app_name, error_value) |
121 | self.mocker.replay() |
122 | self.login.emit_user_registration_error(app_name, raised_error) |
123 | yield client.unregister_to_signals() |
124 | @@ -275,14 +282,17 @@ |
125 | def test_emit_login_error(self): |
126 | """Test that the db was called.""" |
127 | app_name = 'app' |
128 | - raised_error = 'error' |
129 | + error_value = 'error' |
130 | + raised_error = self.mocker.mock() |
131 | |
132 | @defer.inlineCallbacks |
133 | def test_emit(client): |
134 | """Actual test.""" |
135 | - self.except_to_errdict(raised_error) |
136 | - self.mocker.result(raised_error) |
137 | - client.on_login_error_cb(app_name, raised_error) |
138 | + raised_error.value |
139 | + self.mocker.result(error_value) |
140 | + self.except_to_errdict(error_value) |
141 | + self.mocker.result(error_value) |
142 | + client.on_login_error_cb(app_name, error_value) |
143 | self.mocker.replay() |
144 | self.login.emit_login_error(app_name, raised_error) |
145 | yield client.unregister_to_signals() |
146 | @@ -361,14 +371,17 @@ |
147 | def test_emit_email_validation_error(self): |
148 | """Test the cb was called.""" |
149 | app_name = 'app' |
150 | - raised_error = 'error' |
151 | + error_value = 'error' |
152 | + raised_error = self.mocker.mock() |
153 | |
154 | @defer.inlineCallbacks |
155 | def test_emit(client): |
156 | """Actual test.""" |
157 | - self.except_to_errdict(raised_error) |
158 | - self.mocker.result(raised_error) |
159 | - client.on_email_validation_error_cb(app_name, raised_error) |
160 | + raised_error.value |
161 | + self.mocker.result(error_value) |
162 | + self.except_to_errdict(error_value) |
163 | + self.mocker.result(error_value) |
164 | + client.on_email_validation_error_cb(app_name, error_value) |
165 | self.mocker.replay() |
166 | self.login.emit_email_validation_error(app_name, raised_error) |
167 | yield client.unregister_to_signals() |
168 | @@ -427,14 +440,17 @@ |
169 | def test_emit_password_reset_error(self): |
170 | """Test the cb was called.""" |
171 | app_name = 'app' |
172 | - raised_error = 'error' |
173 | + error_value = 'error' |
174 | + raised_error = self.mocker.mock() |
175 | |
176 | @defer.inlineCallbacks |
177 | def test_emit(client): |
178 | """Actual test.""" |
179 | - self.except_to_errdict(raised_error) |
180 | - self.mocker.result(raised_error) |
181 | - client.on_password_reset_error_cb(app_name, raised_error) |
182 | + raised_error.value |
183 | + self.mocker.result(error_value) |
184 | + self.except_to_errdict(error_value) |
185 | + self.mocker.result(error_value) |
186 | + client.on_password_reset_error_cb(app_name, error_value) |
187 | self.mocker.replay() |
188 | self.login.emit_password_reset_error(app_name, raised_error) |
189 | yield client.unregister_to_signals() |
190 | @@ -491,14 +507,17 @@ |
191 | def test_emit_password_change_error(self): |
192 | """Test the cb was called.""" |
193 | app_name = 'app' |
194 | - raised_error = 'error' |
195 | + error_value = 'error' |
196 | + raised_error = self.mocker.mock() |
197 | |
198 | @defer.inlineCallbacks |
199 | def test_emit(client): |
200 | """Actual test.""" |
201 | - self.except_to_errdict(raised_error) |
202 | - self.mocker.result(raised_error) |
203 | - client.on_password_change_error(app_name, raised_error) |
204 | + raised_error.value |
205 | + self.mocker.result(error_value) |
206 | + self.except_to_errdict(error_value) |
207 | + self.mocker.result(error_value) |
208 | + client.on_password_change_error(app_name, error_value) |
209 | self.mocker.replay() |
210 | self.login.emit_password_change_error(app_name, raised_error) |
211 | yield client.unregister_to_signals() |
212 | |
213 | === modified file 'ubuntu_sso/main/windows.py' |
214 | --- ubuntu_sso/main/windows.py 2011-03-30 19:27:48 +0000 |
215 | +++ ubuntu_sso/main/windows.py 2011-04-12 15:24:35 +0000 |
216 | @@ -15,7 +15,6 @@ |
217 | # You should have received a copy of the GNU General Public License along |
218 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
219 | """Main implementation on windows.""" |
220 | - |
221 | from functools import wraps |
222 | from twisted.internet import defer, reactor |
223 | from twisted.internet.threads import deferToThread |
224 | @@ -61,7 +60,7 @@ |
225 | from ubuntu_sso.main import (CredentialsManagementRoot, SSOLoginRoot, |
226 | SSOCredentialsRoot, except_to_errdict) |
227 | |
228 | -logger = setup_logging("ubuntu_sso.main") |
229 | +logger = setup_logging("ubuntu_sso.main.windows") |
230 | NAMED_PIPE_URL = '\\\\.\\pipe\\ubuntu_sso\\%s' |
231 | |
232 | |
233 | @@ -74,9 +73,11 @@ |
234 | |
235 | def blocking(f, app_name, result_cb, error_cb): |
236 | """Run f in a thread; return or throw an exception thru the callbacks.""" |
237 | - d = deferToThread(f, app_name) |
238 | - d.addCallbacks(result_cb, error_cb, callbackArgs=(app_name,), |
239 | - errbackArgs=(app_name,)) |
240 | + d = deferToThread(f) |
241 | + # the calls in twisted will be called with the args in a diff order, |
242 | + # in order to follow the linux api, we swap them around with a lambda |
243 | + d.addCallback(lambda result, app: result_cb(app, result), app_name) |
244 | + d.addErrback(lambda err, app: error_cb(app, err), app_name) |
245 | |
246 | |
247 | class RemoteMeta(type): |
248 | @@ -164,7 +165,7 @@ |
249 | logger.debug('SSOLogin: emitting CaptchaGenerationError with ' |
250 | 'app_name "%s" and error %r', app_name, raised_error) |
251 | self.emit_signal('on_captcha_generation_error', app_name, |
252 | - except_to_errdict(raised_error)) |
253 | + except_to_errdict(raised_error.value)) |
254 | |
255 | def generate_captcha(self, app_name, filename): |
256 | """Call the matching method in the processor.""" |
257 | @@ -184,7 +185,7 @@ |
258 | logger.debug('SSOLogin: emitting UserRegistrationError with ' |
259 | 'app_name "%s" and error %r', app_name, raised_error) |
260 | self.emit_signal('on_user_registration_error', app_name, |
261 | - except_to_errdict(raised_error)) |
262 | + except_to_errdict(raised_error.value)) |
263 | |
264 | def register_user(self, app_name, email, password, displayname, |
265 | captcha_id, captcha_solution): |
266 | @@ -206,7 +207,7 @@ |
267 | logger.debug('SSOLogin: emitting LoginError with ' |
268 | 'app_name "%s" and error %r', app_name, raised_error) |
269 | self.emit_signal('on_login_error', app_name, |
270 | - except_to_errdict(raised_error)) |
271 | + except_to_errdict(raised_error.value)) |
272 | |
273 | def emit_user_not_validated(self, app_name, result): |
274 | """Signal thrown when the user is not validated.""" |
275 | @@ -232,7 +233,7 @@ |
276 | logger.debug('SSOLogin: emitting EmailValidationError with ' |
277 | 'app_name "%s" and error %r', app_name, raised_error) |
278 | self.emit_signal('on_email_validation_error', app_name, |
279 | - except_to_errdict(raised_error)) |
280 | + except_to_errdict(raised_error.value)) |
281 | |
282 | def validate_email(self, app_name, email, password, email_token): |
283 | """Call the matching method in the processor.""" |
284 | @@ -252,7 +253,7 @@ |
285 | logger.debug('SSOLogin: emitting PasswordResetError with ' |
286 | 'app_name "%s" and error %r', app_name, raised_error) |
287 | self.emit_signal('on_password_reset_error', app_name, |
288 | - except_to_errdict(raised_error)) |
289 | + except_to_errdict(raised_error.value)) |
290 | |
291 | def request_password_reset_token(self, app_name, email): |
292 | """Call the matching method in the processor.""" |
293 | @@ -272,7 +273,7 @@ |
294 | logger.debug('SSOLogin: emitting PasswordChangeError with ' |
295 | 'app_name "%s" and error %r', app_name, raised_error) |
296 | self.emit_signal('on_password_change_error', app_name, |
297 | - except_to_errdict(raised_error)) |
298 | + except_to_errdict(raised_error.value)) |
299 | |
300 | def set_new_password(self, app_name, email, token, new_password): |
301 | """Call the matching method in the processor.""" |
302 | @@ -918,9 +919,12 @@ |
303 | @defer.inlineCallbacks |
304 | def _request_remote_objects(self, root): |
305 | """Get the status remote object.""" |
306 | - self.sso_login = yield root.callRemote('get_sso_login') |
307 | - self.sso_cred = yield root.callRemote('get_sso_credentials') |
308 | - self.cred_management = yield root.callRemote('get_cred_manager') |
309 | + sso_login = yield root.callRemote('get_sso_login') |
310 | + self.sso_login = SSOLoginClient(sso_login) |
311 | + sso_cred = yield root.callRemote('get_sso_credentials') |
312 | + self.sso_cred = SSOCredentialsClient(sso_cred) |
313 | + cred_management = yield root.callRemote('get_cred_manager') |
314 | + self.cred_management = CredentialsManagementClient(cred_management) |
315 | defer.returnValue(self) |
316 | |
317 | def connect(self): |
318 | |
319 | === modified file 'ubuntu_sso/qt/controllers.py' |
320 | --- ubuntu_sso/qt/controllers.py 2011-04-05 13:37:33 +0000 |
321 | +++ ubuntu_sso/qt/controllers.py 2011-04-12 15:24:35 +0000 |
322 | @@ -16,13 +16,21 @@ |
323 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
324 | """Controllers with the logic of the UI.""" |
325 | |
326 | +import os |
327 | +import tempfile |
328 | + |
329 | +from PyQt4.QtGui import QMessageBox |
330 | from PyQt4.QtCore import QUrl |
331 | +from twisted.internet.defer import inlineCallbacks, returnValue |
332 | |
333 | from ubuntu_sso.logger import setup_logging |
334 | +from ubuntu_sso.main.windows import UbuntuSSOClient |
335 | from ubuntu_sso.utils.ui import ( |
336 | CAPTCHA_SOLUTION_ENTRY, |
337 | EMAIL1_ENTRY, |
338 | EMAIL2_ENTRY, |
339 | + EMAIL_MISMATCH, |
340 | + EMAIL_INVALID, |
341 | EXISTING_ACCOUNT_CHOICE_BUTTON, |
342 | FORGOTTEN_PASSWORD_BUTTON, |
343 | JOIN_HEADER_LABEL, |
344 | @@ -30,9 +38,12 @@ |
345 | PASSWORD1_ENTRY, |
346 | PASSWORD2_ENTRY, |
347 | PASSWORD_HELP, |
348 | + PASSWORD_MISMATCH, |
349 | + PASSWORD_TOO_WEAK, |
350 | SET_UP_ACCOUNT_CHOICE_BUTTON, |
351 | SET_UP_ACCOUNT_BUTTON, |
352 | SIGN_IN_BUTTON, |
353 | + SUCCESS, |
354 | SURNAME_ENTRY, |
355 | TC_BUTTON, |
356 | VERIFY_EMAIL_TITLE, |
357 | @@ -45,6 +56,36 @@ |
358 | logger = setup_logging('ubuntu_sso.controllers') |
359 | |
360 | |
361 | +class BackendController(object): |
362 | + """Represent a controller that talks with the sso backend.""" |
363 | + |
364 | + def __init__(self): |
365 | + """Create a new instance.""" |
366 | + self.root = None |
367 | + self.backends = [] |
368 | + |
369 | + def __del__(self): |
370 | + """Clean the resources.""" |
371 | + logger.info('Unregistering %s backends', len(self.backends)) |
372 | + for backend in self.backends: |
373 | + backend.unregister_to_signals() |
374 | + if self.root is not None: |
375 | + logger.info('Disconnecting from root.') |
376 | + self.root.disconnect() |
377 | + |
378 | + @inlineCallbacks |
379 | + def get_backend(self): |
380 | + """Return the backend used by the controller.""" |
381 | + # get the back end from the root |
382 | + if self.root is None: |
383 | + self.root = UbuntuSSOClient() |
384 | + self.root = yield self.root.connect() |
385 | + backend = self.root.sso_login |
386 | + yield backend.register_to_signals() |
387 | + self.backends.append(backend) |
388 | + returnValue(backend) |
389 | + |
390 | + |
391 | class ChooseSignInController(object): |
392 | """Controlled to the ChooseSignIn view/widget.""" |
393 | |
394 | @@ -87,12 +128,17 @@ |
395 | view.wizard().next() |
396 | |
397 | |
398 | -class CurrentUserController(object): |
399 | +class CurrentUserController(BackendController): |
400 | """Controller used in the view that is used to allow the signin.""" |
401 | |
402 | - def __init__(self, title=''): |
403 | + def __init__(self, backend=None, title='', app_name='', message_box=None): |
404 | """Create a new instance.""" |
405 | + super(CurrentUserController, self).__init__() |
406 | + if message_box is None: |
407 | + message_box = QMessageBox |
408 | + self.message_box = message_box |
409 | self._title = title |
410 | + self._app_name = app_name |
411 | |
412 | def _set_translated_strings(self, view): |
413 | """Set the translated strings.""" |
414 | @@ -102,21 +148,56 @@ |
415 | view.forgot_password_label.setText(FORGOTTEN_PASSWORD_BUTTON) |
416 | view.sign_in_button.setText(SIGN_IN_BUTTON) |
417 | |
418 | + def _connect_buttons(self, view, backend): |
419 | + """Connect the buttons to perform actions.""" |
420 | + view.sign_in_button.clicked.connect(lambda: self.login(view, backend)) |
421 | + # lets add call backs to be execute for the calls we are interested |
422 | + backend.on_login_error_cb = lambda app, error:\ |
423 | + self.on_login_error(view, app, error) |
424 | + backend.on_logged_in_cb = lambda app, result:\ |
425 | + self.on_logged_in(view, app, result) |
426 | + |
427 | + def login(self, view, backend): |
428 | + """Perform the login using the backend.""" |
429 | + logger.debug('Trying to log in using %s', backend) |
430 | + # grab the data from the view and call the backend |
431 | + d = backend.login(self._app_name, view.email, view.password) |
432 | + d.addErrback(lambda e: self.on_login_error(view, self._app_name, e)) |
433 | + |
434 | + def on_login_error(self, view, app_name, error): |
435 | + """There was an error when login in.""" |
436 | + # let the user know |
437 | + logger.error('Got error when login %s, error: %s', app_name, error) |
438 | + self.message_box.critical(view, app_name, error) |
439 | + |
440 | + def on_logged_in(self, view, app_name, result): |
441 | + """We managed to log in.""" |
442 | + logger.info('Logged in for %s', app_name) |
443 | + self.message_box.information(view, app_name, SUCCESS) |
444 | + view.wizard.close() |
445 | + |
446 | # use an ugly name just so have a simlar api as found in PyQt |
447 | #pylint: disable=C0103 |
448 | + @inlineCallbacks |
449 | def setupUi(self, view): |
450 | """Setup the view.""" |
451 | + backend = yield self.get_backend() |
452 | view.setTitle(self._title) |
453 | self._set_translated_strings(view) |
454 | + self._connect_buttons(view, backend) |
455 | #pylint: enable=C0103 |
456 | |
457 | |
458 | -class SetUpAccountController(object): |
459 | +class SetUpAccountController(BackendController): |
460 | """Conroller for the setup account view.""" |
461 | |
462 | def __init__(self, tos_id=0, validation_id=1, app_name='', |
463 | - help_message=''): |
464 | + help_message='', message_box=None): |
465 | """Create a new instance.""" |
466 | + super(SetUpAccountController, self).__init__() |
467 | + if message_box is None: |
468 | + message_box = QMessageBox |
469 | + self.message_box = message_box |
470 | self._tos_id = tos_id |
471 | self._validation_id = validation_id |
472 | self._app_name = app_name |
473 | @@ -157,31 +238,90 @@ |
474 | view.password_edit.textChanged.connect( |
475 | view.confirm_password_edit.textChanged.emit) |
476 | |
477 | - def _connect_ui_elements(self, view): |
478 | + def _connect_ui_elements(self, view, backend): |
479 | """Set the connection of signals.""" |
480 | view.terms_and_conditions_button.clicked.connect( |
481 | lambda: self.set_next_tos(view)) |
482 | view.set_up_button.clicked.connect(lambda: self.set_next_validation( |
483 | - view)) |
484 | + view, backend)) |
485 | view.password_edit.textChanged.connect( |
486 | lambda x: self.update_password_strength(x, view)) |
487 | view.terms_and_conditions_check_box.stateChanged.connect( |
488 | view.set_up_button.setEnabled) |
489 | + view.captcha_refresh_label.linkActivated.connect(lambda url:\ |
490 | + self._refresh_captcha(view, backend)) |
491 | + # set the callbacks for the captcha generation |
492 | + backend.on_captcha_generated_cb = lambda app, result:\ |
493 | + self.on_captcha_generated(view, app, result) |
494 | + backend.on_captcha_generation_error_cb = lambda app, error:\ |
495 | + self.on_captcha_generation_error(view, app, error) |
496 | + backend.on_user_registration_error_cb = lambda app, error:\ |
497 | + self.on_user_registration_error(view, app, error) |
498 | + return backend |
499 | + |
500 | + def _refresh_captcha(self, view, backend): |
501 | + """Refresh the captcha image shown in the ui.""" |
502 | + # lets clean behind us, do we have the old file arround? |
503 | + old_file = None |
504 | + if view.captcha_file and os.path.exists(view.captcha_file): |
505 | + old_file = view.captcha_file |
506 | + fd = tempfile.TemporaryFile(mode='r') |
507 | + file_name = fd.name |
508 | + view.captcha_file = file_name |
509 | + d = backend.generate_captcha(self._app_name, file_name) |
510 | + if old_file: |
511 | + d.addCallback(lambda x: os.unlink(old_file)) |
512 | + d.addErrback(lambda e: self.on_captcha_generation_error(view, |
513 | + self._app_name, |
514 | + e)) |
515 | |
516 | def _set_titles(self, view): |
517 | """Set the diff titles of the view.""" |
518 | view.setTitle(JOIN_HEADER_LABEL % {'app_name': self._app_name}) |
519 | view.setSubTitle(self._help_message) |
520 | |
521 | + def on_captcha_generated(self, view, app_name, result): |
522 | + """A new image was generated.""" |
523 | + view.captcha_image = result |
524 | + |
525 | + def on_captcha_generation_error(self, view, app_name, error): |
526 | + """An error ocurred.""" |
527 | + self.message_box.critical(view, app_name, str(error)) |
528 | + |
529 | + def on_user_registration_error(self, view, app_name, error): |
530 | + """Let the user know we could not register.""" |
531 | + # error are returned as a dict with the data we want to show. |
532 | + self.message_box.critical(view, error['errtype'], error['email']) |
533 | + |
534 | def set_next_tos(self, view): |
535 | """Set the tos page as the next one.""" |
536 | view.next = self._tos_id |
537 | view.wizard().next() |
538 | |
539 | - def set_next_validation(self, view): |
540 | + def validate_form(self, view): |
541 | + """Validate the info of the form and return an error.""" |
542 | + if not self.is_correct_email(view.email): |
543 | + self.message_box.critical(view, self._app_name, EMAIL_INVALID) |
544 | + if view.email_edit.text() != view.confirm_email_edit.text(): |
545 | + self.message_box.critical(view, self._app_name, EMAIL_MISMATCH) |
546 | + return False |
547 | + if not is_min_required_password(str(view.password_edit.text())): |
548 | + self.message_box.critical(view, self._app_name, PASSWORD_TOO_WEAK) |
549 | + return False |
550 | + if view.password_edit.text() != view.confirm_password_edit.text(): |
551 | + self.message_box.critical(view, self._app_name, PASSWORD_MISMATCH) |
552 | + return False |
553 | + return True |
554 | + |
555 | + def set_next_validation(self, view, backend): |
556 | """Set the validation as the next page.""" |
557 | - view.next = self._validation_id |
558 | - view.wizard().next() |
559 | + # validate the current info of the form, try to perform the action |
560 | + # to register the user, and then move foward |
561 | + if self.validate_form(view): |
562 | + backend.register_user(self._app_name, view.email, view.password, |
563 | + view.captcha_id, view.captcha_solution) |
564 | + view.next = self._validation_id |
565 | + view.wizard().next() |
566 | |
567 | def update_password_strength(self, password, view): |
568 | """Callback used to update the password strenght UI code.""" |
569 | @@ -203,12 +343,16 @@ |
570 | return view.password_edit.text() == password |
571 | |
572 | #pylint: disable=C0103 |
573 | + @inlineCallbacks |
574 | def setupUi(self, view): |
575 | """Setup the view.""" |
576 | + # request the backend to be used with the ui |
577 | + backend = yield self.get_backend() |
578 | + self._connect_ui_elements(view, backend) |
579 | + self._refresh_captcha(view, backend) |
580 | self._set_titles(view) |
581 | self._set_translated_strings(view) |
582 | self._set_line_edits_validations(view) |
583 | - self._connect_ui_elements(view) |
584 | #pylint: enable=C0103 |
585 | |
586 | |
587 | |
588 | === modified file 'ubuntu_sso/qt/gui.py' |
589 | --- ubuntu_sso/qt/gui.py 2011-04-05 13:37:33 +0000 |
590 | +++ ubuntu_sso/qt/gui.py 2011-04-12 15:24:35 +0000 |
591 | @@ -16,7 +16,6 @@ |
592 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
593 | """Qt implementation of the UI.""" |
594 | |
595 | -from PyQt4 import Qt |
596 | from PyQt4.QtGui import ( |
597 | QApplication, |
598 | QColor, |
599 | @@ -27,8 +26,7 @@ |
600 | QPushButton, |
601 | QStyle, |
602 | QWizard, |
603 | - QWizardPage, |
604 | - QGraphicsScene) |
605 | + QWizardPage) |
606 | |
607 | from ubuntu_sso.logger import setup_logging |
608 | # pylint: disable=F0401,E0611 |
609 | @@ -50,7 +48,7 @@ |
610 | |
611 | # colors used to define the password stenght |
612 | WEAK_COLOR = QColor(220, 20, 60) |
613 | -MEDIUN_COLOR = QColor(255, 215, 0) |
614 | +MEDIUM_COLOR = QColor(255, 215, 0) |
615 | STRONG_COLOR = QColor(50, 205, 50) |
616 | |
617 | |
618 | @@ -224,13 +222,11 @@ |
619 | # palettes that will be used to set the colors of the password strenght |
620 | original_palette = self.ui.strenght_frame.palette() |
621 | self._original_color = original_palette.background().color() |
622 | + self.next = -1 |
623 | + self.captcha_id = None |
624 | + self.captcha_file = None |
625 | self.controller = controller |
626 | self.controller.setupUi(self) |
627 | - self.next = -1 |
628 | - # create the scene to be used to show the captcha |
629 | - self.image = None |
630 | - self.image_file = None |
631 | - self.scene = QGraphicsScene(self.ui.captcha_view) |
632 | |
633 | def set_strenght_level(self, level, password): |
634 | """Set the strenght level colors.""" |
635 | @@ -348,6 +344,11 @@ |
636 | return self.ui.confirm_password_edit |
637 | |
638 | @property |
639 | + def captcha_refresh_label(self): |
640 | + """Return the refresh label.""" |
641 | + return self.ui.refresh_label |
642 | + |
643 | + @property |
644 | def captcha_solution(self): |
645 | """Return the provided captcha solution.""" |
646 | return str(self.ui.captcha_solution_edit.text()) |
647 | @@ -359,15 +360,14 @@ |
648 | |
649 | def get_captcha_image(self): |
650 | """Return the path to the captcha image.""" |
651 | - return self.image_file |
652 | + return self.captcha_id |
653 | |
654 | - def set_captcha_image(self, file_path): |
655 | + def set_captcha_image(self, data): |
656 | """Set the new image of the captcha.""" |
657 | - if self.image: |
658 | - self.scene.removeItem(self.image) |
659 | - self.image_file = file_path |
660 | - pix = QPixmap(self.image_file) |
661 | - self.image = self.scene.addPixmap(pix) |
662 | + self.captcha_id = data |
663 | + # lets set the QPixmap for the label |
664 | + pic = QPixmap(self.captcha_file) |
665 | + self.ui.captcha_view.setPixmap(pic) |
666 | |
667 | captcha_image = property(get_captcha_image, set_captcha_image) |
668 | |
669 | @@ -416,7 +416,8 @@ |
670 | self.email_verification = EmailVerificationPage( |
671 | Ui_EmailVerificationPage(), |
672 | EmailVerificationController()) |
673 | - self.current_user_controller = CurrentUserController(title='Sign in') |
674 | + self.current_user_controller = CurrentUserController(title='Sign in', |
675 | + app_name=app_name) |
676 | self.current_user = CurrentUserSignInPage(Ui_CurrentUserSignInPage(), |
677 | self.current_user_controller, |
678 | parent=self) |
679 | @@ -445,15 +446,3 @@ |
680 | |
681 | class UbuntuSSOClientGUI(object): |
682 | """Ubuntu single sign-on GUI.""" |
683 | - |
684 | -if __name__ == '__main__': |
685 | - # pylint: disable=C0103 |
686 | - # show the ui |
687 | - import sys |
688 | - |
689 | - app = Qt.QApplication(sys.argv) |
690 | - |
691 | - wizard = UbuntuSSOWizard() |
692 | - wizard.show() |
693 | - # Now we can start it. |
694 | - app.exec_() |
695 | |
696 | === added file 'ubuntu_sso/qt/tests/show_gui.py' |
697 | --- ubuntu_sso/qt/tests/show_gui.py 1970-01-01 00:00:00 +0000 |
698 | +++ ubuntu_sso/qt/tests/show_gui.py 2011-04-12 15:24:35 +0000 |
699 | @@ -0,0 +1,33 @@ |
700 | +# -*- coding: utf-8 -*- |
701 | +# Author: Manuel de la Pena <manuel@canonical.com> |
702 | +# |
703 | +# Copyright 2011 Canonical Ltd. |
704 | +# |
705 | +# This program is free software: you can redistribute it and/or modify it |
706 | +# under the terms of the GNU General Public License version 3, as published |
707 | +# by the Free Software Foundation. |
708 | +# |
709 | +# This program is distributed in the hope that it will be useful, but |
710 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
711 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
712 | +# PURPOSE. See the GNU General Public License for more details. |
713 | +# |
714 | +# You should have received a copy of the GNU General Public License along |
715 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
716 | +"""Script that shows the qt gui.""" |
717 | + |
718 | + |
719 | +if __name__ == '__main__': |
720 | + # pylint: disable=C0103,E1101,F0401 |
721 | + # show the ui |
722 | + import sys |
723 | + from PyQt4 import Qt |
724 | + app = Qt.QApplication(sys.argv) |
725 | + from qtreactor import qt4reactor |
726 | + qt4reactor.install() |
727 | + from twisted.internet import reactor |
728 | + from ubuntu_sso.qt.gui import UbuntuSSOWizard |
729 | + |
730 | + wizard = UbuntuSSOWizard(app_name='test') |
731 | + wizard.show() |
732 | + reactor.run() |
733 | |
734 | === renamed file 'ubuntu_sso/qt/tests/test_controllers.py' => 'ubuntu_sso/qt/tests/test_windows.py' |
735 | --- ubuntu_sso/qt/tests/test_controllers.py 2011-04-05 13:45:31 +0000 |
736 | +++ ubuntu_sso/qt/tests/test_windows.py 2011-04-12 15:24:35 +0000 |
737 | @@ -28,6 +28,7 @@ |
738 | CAPTCHA_SOLUTION_ENTRY, |
739 | EMAIL1_ENTRY, |
740 | EMAIL2_ENTRY, |
741 | + EMAIL_MISMATCH, |
742 | EXISTING_ACCOUNT_CHOICE_BUTTON, |
743 | FORGOTTEN_PASSWORD_BUTTON, |
744 | JOIN_HEADER_LABEL, |
745 | @@ -35,6 +36,8 @@ |
746 | PASSWORD1_ENTRY, |
747 | PASSWORD2_ENTRY, |
748 | PASSWORD_HELP, |
749 | + PASSWORD_TOO_WEAK, |
750 | + PASSWORD_MISMATCH, |
751 | SET_UP_ACCOUNT_CHOICE_BUTTON, |
752 | SET_UP_ACCOUNT_BUTTON, |
753 | SIGN_IN_BUTTON, |
754 | @@ -95,17 +98,28 @@ |
755 | """Setup tests.""" |
756 | super(CurrentUserControllerTestCase, self).setUp() |
757 | self.view = self.mocker.mock() |
758 | - self.controller = CurrentUserController(title='the title') |
759 | + self.backend = self.mocker.mock() |
760 | + self.connect = self.mocker.mock() |
761 | + self.signal = self.mocker.replace('PyQt4.QtCore.SIGNAL') |
762 | + self.controller = CurrentUserController(title='the title', |
763 | + backend=self.backend) |
764 | |
765 | - def test_setup_ui(self): |
766 | + def test_translated_strings(self): |
767 | """test that the ui is correctly set up.""" |
768 | - self.view.setTitle(self.controller._title) |
769 | self.view.email_edit.setPlaceholderText(EMAIL1_ENTRY) |
770 | self.view.password_edit.setPlaceholderText(PASSWORD1_ENTRY) |
771 | self.view.forgot_password_label.setText(FORGOTTEN_PASSWORD_BUTTON) |
772 | self.view.sign_in_button.setText(SIGN_IN_BUTTON) |
773 | self.mocker.replay() |
774 | - self.controller.setupUi(self.view) |
775 | + self.controller._set_translated_strings(self.view) |
776 | + |
777 | + def test_connect_ui(self): |
778 | + """test that the ui is correctly set up.""" |
779 | + self.view.sign_in_button.clicked.connect(MATCH(callable)) |
780 | + self.backend.on_login_error_cb = MATCH(callable) |
781 | + self.backend.on_logged_in_cb = MATCH(callable) |
782 | + self.mocker.replay() |
783 | + self.controller._connect_buttons(self.view, self.backend) |
784 | |
785 | |
786 | class SetUpAccountControllerTestCase(MockerTestCase): |
787 | @@ -119,8 +133,10 @@ |
788 | 'ubuntu_sso.utils.ui.get_password_strength') |
789 | self.app_name = 'test' |
790 | self.help = 'help' |
791 | + self.message_box = self.mocker.mock() |
792 | self.controller = SetUpAccountController(app_name=self.app_name, |
793 | - help_message=self.help) |
794 | + help_message=self.help, |
795 | + message_box=self.message_box) |
796 | |
797 | def test_set_translated_strings(self): |
798 | """Ensure all the strings are set.""" |
799 | @@ -148,15 +164,21 @@ |
800 | |
801 | def test_connect_ui_elements(self): |
802 | """Test that the ui elements are correctly connect.""" |
803 | + backend = self.mocker.mock() |
804 | self.view.terms_and_conditions_button.clicked.connect(MATCH(callable)) |
805 | self.view.set_up_button.clicked.connect(MATCH(callable)) |
806 | self.view.password_edit.textChanged.connect(MATCH(callable)) |
807 | self.view.set_up_button.setEnabled |
808 | - self.mocker.result(lambda:None) |
809 | + self.mocker.result(lambda: None) |
810 | self.view.terms_and_conditions_check_box.stateChanged.connect( |
811 | MATCH(callable)) |
812 | + self.view.captcha_refresh_label.linkActivated.connect(MATCH(callable)) |
813 | + # set the callbacks for the captcha generatio |
814 | + backend.on_captcha_generated_cb = MATCH(callable) |
815 | + backend.on_captcha_generation_error_cb = MATCH(callable) |
816 | + backend.on_user_registration_error_cb = MATCH(callable) |
817 | self.mocker.replay() |
818 | - self.controller._connect_ui_elements(self.view) |
819 | + self.controller._connect_ui_elements(self.view, backend) |
820 | |
821 | def test_is_correct_password_confirmation_false(self): |
822 | """Test when the password is not correct.""" |
823 | @@ -219,10 +241,86 @@ |
824 | |
825 | def test_set_next_validation(self): |
826 | """Test the callback.""" |
827 | + email = 'email@example.com' |
828 | + password = 'Qwerty9923' |
829 | + captcha_id = 'captcha_id' |
830 | + captcha_solution = 'captcha_solution' |
831 | + self.view.email |
832 | + self.mocker.result(email) |
833 | + self.view.email_edit.text() |
834 | + self.mocker.result(email) |
835 | + self.view.confirm_email_edit.text() |
836 | + self.mocker.result(email) |
837 | + self.view.password_edit.text() |
838 | + self.mocker.result(password) |
839 | + self.view.password_edit.text() |
840 | + self.mocker.result(password) |
841 | + self.view.confirm_password_edit.text() |
842 | + self.mocker.result(password) |
843 | + self.view.email |
844 | + self.mocker.result(email) |
845 | + self.view.password |
846 | + self.mocker.result(password) |
847 | + self.view.captcha_id |
848 | + self.mocker.result(captcha_id) |
849 | + self.view.captcha_solution |
850 | + self.mocker.result(captcha_solution) |
851 | + backend = self.mocker.mock() |
852 | + backend.register_user(self.app_name, email, password, captcha_id, |
853 | + captcha_solution) |
854 | self.view.next = self.controller._validation_id |
855 | self.view.wizard().next() |
856 | self.mocker.replay() |
857 | - self.controller.set_next_validation(self.view) |
858 | + self.controller.set_next_validation(self.view, backend) |
859 | + |
860 | + def test_set_next_validation_wrong_email(self): |
861 | + """Test the callback when there is a wrong email.""" |
862 | + email = 'email@example.com' |
863 | + self.view.email |
864 | + self.mocker.result(email) |
865 | + self.view.email_edit.text() |
866 | + self.mocker.result('email') |
867 | + self.view.confirm_email_edit.text() |
868 | + self.mocker.result('email2') |
869 | + self.message_box.critical(self.view, self.app_name, EMAIL_MISMATCH) |
870 | + self.mocker.replay() |
871 | + self.assertFalse(self.controller.validate_form(self.view)) |
872 | + |
873 | + def test_set_next_validation_not_min_password(self): |
874 | + """Test the callback with a weak password.""" |
875 | + weak_password = 'weak' |
876 | + email = 'email@example.com' |
877 | + self.view.email |
878 | + self.mocker.result(email) |
879 | + self.view.email_edit.text() |
880 | + self.mocker.result(email) |
881 | + self.view.confirm_email_edit.text() |
882 | + self.mocker.result(email) |
883 | + self.view.password_edit.text() |
884 | + self.mocker.result(weak_password) |
885 | + self.message_box.critical(self.view, self.app_name, PASSWORD_TOO_WEAK) |
886 | + self.mocker.replay() |
887 | + self.assertFalse(self.controller.validate_form(self.view)) |
888 | + |
889 | + def test_set_next_validation_wrong_password(self): |
890 | + """Test the callback where the password is wrong.""" |
891 | + password = 'Qwerty9923' |
892 | + email = 'email@example.com' |
893 | + self.view.email |
894 | + self.mocker.result(email) |
895 | + self.view.email_edit.text() |
896 | + self.mocker.result(email) |
897 | + self.view.confirm_email_edit.text() |
898 | + self.mocker.result(email) |
899 | + self.view.password_edit.text() |
900 | + self.mocker.result(password) |
901 | + self.view.password_edit.text() |
902 | + self.mocker.result(password) |
903 | + self.view.confirm_password_edit.text() |
904 | + self.mocker.result('other_password') |
905 | + self.message_box.critical(self.view, self.app_name, PASSWORD_MISMATCH) |
906 | + self.mocker.replay() |
907 | + self.assertFalse(self.controller.validate_form(self.view)) |
908 | |
909 | def test_update_password_strength(self): |
910 | """Test the callback.""" |
+1