Merge lp:~bac/launchpad/bug-452491-captcha2-boogaloo into lp:launchpad
- bug-452491-captcha2-boogaloo
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | not available | ||||
Proposed branch: | lp:~bac/launchpad/bug-452491-captcha2-boogaloo | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
451 lines 9 files modified
lib/canonical/launchpad/browser/tests/registration.py (+1/-1) lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt (+1/-1) lib/canonical/launchpad/templates/launchpad-forgottenpassword.pt (+34/-7) lib/canonical/launchpad/templates/launchpad-login.pt (+1/-0) lib/canonical/launchpad/webapp/login.py (+48/-38) lib/lp/registry/stories/foaf/xx-createaccount.txt (+2/-2) lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt (+4/-4) lib/lp/registry/stories/foaf/xx-resetpassword.txt (+34/-24) lib/lp/testing/registration.py (+2/-2) |
||||
To merge this branch: | bzr merge lp:~bac/launchpad/bug-452491-captcha2-boogaloo | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Albisetti (community) | ui | Approve | |
Abel Deuring (community) | Approve | ||
Review via email: mp+13487@code.launchpad.net |
Commit message
Add simple captcha to the +forgottenpassword page.
Description of the change
Brad Crittenden (bac) wrote : | # |
Abel Deuring (adeuring) wrote : | # |
Hi Brad,
a nice branch r=me. I have only a minor cosmetic suggestion, see below.
Abel
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -24,9 +24,10 @@
> return ''
>
>
> -def set_captcha_
> +def set_captcha_
> """Given a browser, set the login captcha with the correct answer."""
> if answer is None:
> answer = get_captcha_
> - browser.
> + control_name = prefix + 'captcha_
> + browser.
> answer)
I think you can merge the last two lines in one ;)
Brad Crittenden (bac) wrote : | # |
> Hi Brad,
>
> a nice branch r=me. I have only a minor cosmetic suggestion, see below.
>
> Abel
>
> > === modified file 'lib/lp/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -24,9 +24,10 @@
> > return ''
> >
> >
> > -def set_captcha_
> > +def set_captcha_
> > """Given a browser, set the login captcha with the correct answer."""
> > if answer is None:
> > answer = get_captcha_
> > - browser.
> > + control_name = prefix + 'captcha_
> > + browser.
> > answer)
>
> I think you can merge the last two lines in one ;)
Done
Martin Albisetti (beuno) wrote : | # |
Bonus points if the forms look like everywhere else in Launchpad.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/browser/tests/registration.py' |
2 | --- lib/canonical/launchpad/browser/tests/registration.py 2009-10-08 19:52:06 +0000 |
3 | +++ lib/canonical/launchpad/browser/tests/registration.py 2009-10-16 19:59:17 +0000 |
4 | @@ -21,7 +21,7 @@ |
5 | browser = setupBrowser() |
6 | browser.open('http://launchpad.dev/+login') |
7 | browser.getControl(name='loginpage_email', index=1).value = email |
8 | - set_captcha_answer(browser) |
9 | + set_captcha_answer(browser, prefix='loginpage_') |
10 | browser.getControl('Register').click() |
11 | return browser |
12 | |
13 | |
14 | === modified file 'lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt' |
15 | --- lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt 2009-10-08 19:52:06 +0000 |
16 | +++ lib/canonical/launchpad/pagetests/standalone/xx-new-account-redirection-url.txt 2009-10-16 19:59:17 +0000 |
17 | @@ -42,7 +42,7 @@ |
18 | >>> from lp.testing.registration import set_captcha_answer |
19 | >>> anon_browser.getControl('E-mail address:', index=1).value = ( |
20 | ... 'granny@canonical.com') |
21 | - >>> set_captcha_answer(anon_browser) |
22 | + >>> set_captcha_answer(anon_browser, prefix='loginpage_') |
23 | >>> anon_browser.getControl('Register').click() |
24 | >>> print anon_browser.contents |
25 | <... |
26 | |
27 | === modified file 'lib/canonical/launchpad/templates/launchpad-forgottenpassword.pt' |
28 | --- lib/canonical/launchpad/templates/launchpad-forgottenpassword.pt 2009-07-17 17:59:07 +0000 |
29 | +++ lib/canonical/launchpad/templates/launchpad-forgottenpassword.pt 2009-10-16 19:59:17 +0000 |
30 | @@ -21,22 +21,49 @@ |
31 | |
32 | <div tal:condition="not: view/success"> |
33 | |
34 | - <div class="error" tal:condition="view/errortext" |
35 | - tal:content="structure view/errortext" /> |
36 | + <tal:block condition="view/errortext"> |
37 | + <h1>Your password reset request was unsuccessful</h1> |
38 | + <p class="error message" |
39 | + style="margin-top: 2em;" |
40 | + tal:content="structure view/errortext" /> |
41 | + </tal:block> |
42 | |
43 | <p> |
44 | Enter your e-mail address, and we will send you instructions |
45 | - on how to reset your password. |
46 | + on how to reset your password. You'll also need to answer a simple |
47 | + question so we know that you're human. |
48 | </p> |
49 | |
50 | <form name="forgottenpassword" method="POST"> |
51 | + <input type="hidden" |
52 | + tal:attributes="name view/captcha_hash; |
53 | + value view/get_captcha_hash" /> |
54 | <div class="field"> |
55 | - <label for="email">Email address:</label> |
56 | - <input type="text" size="50" name="email" /> |
57 | + <table> |
58 | + <tr> |
59 | + <th> |
60 | + <label for="email">Email address:</label> |
61 | + </th> |
62 | + <td> |
63 | + <input type="text" size="50" name="email" /> |
64 | + </td> |
65 | + </tr> |
66 | + <tr> |
67 | + <th> |
68 | + <label>Random question:</label> |
69 | + </th> |
70 | + <td> |
71 | + <span id="problem" tal:content="view/captcha_problem" /> |
72 | + <input type="text" size="5" |
73 | + id="captcha_submission" value="" |
74 | + tal:attributes="name view/captcha_submission" |
75 | + /> |
76 | + </td> |
77 | + </tr> |
78 | + </table> |
79 | <div class="formHelp"> |
80 | <p> |
81 | - This should be the email address registered for your Launchpad |
82 | - account. |
83 | + Enter the email address registered for your Launchpad account. |
84 | </p> |
85 | <p> |
86 | If you are claiming an existing account but don't know the email |
87 | |
88 | === modified file 'lib/canonical/launchpad/templates/launchpad-login.pt' |
89 | --- lib/canonical/launchpad/templates/launchpad-login.pt 2009-10-08 15:57:01 +0000 |
90 | +++ lib/canonical/launchpad/templates/launchpad-login.pt 2009-10-16 19:59:17 +0000 |
91 | @@ -164,6 +164,7 @@ |
92 | <tal:block condition="view/registration_error"> |
93 | <h1>Your registration was unsuccessful</h1> |
94 | <p class="error message" |
95 | + style="margin-top: 2em;" |
96 | tal:content="structure view/registration_error" /> |
97 | </tal:block> |
98 | <!-- 5. might want to register: --> |
99 | |
100 | === modified file 'lib/canonical/launchpad/webapp/login.py' |
101 | --- lib/canonical/launchpad/webapp/login.py 2009-10-08 15:57:01 +0000 |
102 | +++ lib/canonical/launchpad/webapp/login.py 2009-10-16 19:59:17 +0000 |
103 | @@ -153,8 +153,44 @@ |
104 | return getUtility(IPersonSet).getByName( |
105 | config.launchpad.restrict_to_team).title |
106 | |
107 | - |
108 | -class LoginOrRegister: |
109 | +class CaptchaMixin: |
110 | + """Mixin class to provide simple captcha capabilities.""" |
111 | + def validateCaptcha(self): |
112 | + """Validate the submitted captcha value matches what we expect.""" |
113 | + expected = self.request.form.get(self.captcha_hash) |
114 | + submitted = self.request.form.get(self.captcha_submission) |
115 | + if expected is not None and submitted is not None: |
116 | + return md5.new(submitted).hexdigest() == expected |
117 | + return False |
118 | + |
119 | + @cachedproperty |
120 | + def captcha_answer(self): |
121 | + """Get the answer for the current captcha challenge. |
122 | + |
123 | + With each failed attempt a new challenge will be given. Our answer |
124 | + space is acknowledged to be ridiculously small but is chosen in the |
125 | + interest of ease-of-use. We're not trying to create an iron-clad |
126 | + challenge but only a minimal obstacle to dumb bots. |
127 | + """ |
128 | + return random.randint(10, 20) |
129 | + |
130 | + @property |
131 | + def get_captcha_hash(self): |
132 | + """Get the captcha hash. |
133 | + |
134 | + The hash is the value we put in the form for later comparison. |
135 | + """ |
136 | + return md5.new(str(self.captcha_answer)).hexdigest() |
137 | + |
138 | + @property |
139 | + def captcha_problem(self): |
140 | + """Create the captcha challenge.""" |
141 | + op1 = random.randint(1, self.captcha_answer - 1) |
142 | + op2 = self.captcha_answer - op1 |
143 | + return '%d + %d =' % (op1, op2) |
144 | + |
145 | + |
146 | +class LoginOrRegister(CaptchaMixin): |
147 | """Merges the former CookieLoginPage and JoinLaunchpadView classes |
148 | to allow the two forms to appear on a single page. |
149 | |
150 | @@ -399,41 +435,6 @@ |
151 | L.append(html % (name, cgi.escape(value, quote=True))) |
152 | return '\n'.join(L) |
153 | |
154 | - def validateCaptcha(self): |
155 | - """Validate the submitted captcha value matches what we expect.""" |
156 | - expected = self.request.form.get(self.captcha_hash) |
157 | - submitted = self.request.form.get(self.captcha_submission) |
158 | - if expected is not None and submitted is not None: |
159 | - return md5.new(submitted).hexdigest() == expected |
160 | - return False |
161 | - |
162 | - @cachedproperty |
163 | - def captcha_answer(self): |
164 | - """Get the answer for the current captcha challenge. |
165 | - |
166 | - With each failed attempt a new challenge will be given. Our answer |
167 | - space is acknowledged to be ridiculously small but is chosen in the |
168 | - interest of ease-of-use. We're not trying to create an iron-clad |
169 | - challenge but only a minimal obstacle to dumb bots. |
170 | - """ |
171 | - return random.randint(10, 20) |
172 | - |
173 | - @property |
174 | - def get_captcha_hash(self): |
175 | - """Get the captcha hash. |
176 | - |
177 | - The hash is the value we put in the form for later comparison. |
178 | - """ |
179 | - return md5.new(str(self.captcha_answer)).hexdigest() |
180 | - |
181 | - @property |
182 | - def captcha_problem(self): |
183 | - """Create the captcha challenge.""" |
184 | - op1 = random.randint(1, self.captcha_answer) |
185 | - op2 = self.captcha_answer - op1 |
186 | - return '%d + %d =' % (op1, op2) |
187 | - |
188 | - |
189 | def logInPrincipal(request, principal, email): |
190 | """Log the principal in. Password validation must be done in callsites.""" |
191 | session = ISession(request) |
192 | @@ -542,16 +543,25 @@ |
193 | return '' |
194 | |
195 | |
196 | -class ForgottenPasswordPage: |
197 | +class ForgottenPasswordPage(CaptchaMixin): |
198 | |
199 | errortext = None |
200 | submitted = False |
201 | + captcha_submission = 'captcha_submission' |
202 | + captcha_hash = 'captcha_hash' |
203 | |
204 | def process_form(self): |
205 | request = self.request |
206 | if request.method != "POST": |
207 | return |
208 | |
209 | + # Validate the user is human, more or less. |
210 | + if not self.validateCaptcha(): |
211 | + self.errortext = ( |
212 | + "The answer to the simple math question was incorrect " |
213 | + "or missing. Please try again.") |
214 | + return |
215 | + |
216 | email = request.form.get("email").strip() |
217 | person = getUtility(IPersonSet).getByEmail(email) |
218 | if person is None: |
219 | |
220 | === modified file 'lib/lp/registry/stories/foaf/xx-createaccount.txt' |
221 | --- lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-10-08 19:52:06 +0000 |
222 | +++ lib/lp/registry/stories/foaf/xx-createaccount.txt 2009-10-16 19:59:17 +0000 |
223 | @@ -36,7 +36,7 @@ |
224 | email address from before has been retained in the form. |
225 | |
226 | >>> from lp.testing.registration import set_captcha_answer |
227 | - >>> set_captcha_answer(browser) |
228 | + >>> set_captcha_answer(browser, prefix='loginpage_') |
229 | >>> browser.getControl('Register').click() |
230 | >>> print_feedback_messages(browser.contents) |
231 | >>> print extract_text(find_main_content(browser.contents)) |
232 | @@ -103,7 +103,7 @@ |
233 | >>> browser.open('http://launchpad.dev/+login') |
234 | >>> browser.getControl(name='loginpage_email', index=1).value = ( |
235 | ... 'jperson@example.com') |
236 | - >>> set_captcha_answer(browser) |
237 | + >>> set_captcha_answer(browser, prefix='loginpage_') |
238 | >>> browser.getControl('Register').click() |
239 | |
240 | >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop() |
241 | |
242 | === modified file 'lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt' |
243 | --- lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt 2009-10-08 19:52:06 +0000 |
244 | +++ lib/lp/registry/stories/foaf/xx-reg-with-existing-email.txt 2009-10-16 19:59:17 +0000 |
245 | @@ -16,7 +16,7 @@ |
246 | >>> browser.getControl('E-mail address:', index=1).value = ( |
247 | ... 'test@canonical.com') |
248 | >>> from lp.testing.registration import set_captcha_answer |
249 | - >>> set_captcha_answer(browser) |
250 | + >>> set_captcha_answer(browser, prefix='loginpage_') |
251 | >>> browser.getControl('Register').click() |
252 | |
253 | >>> for message in get_feedback_messages(browser.contents): |
254 | @@ -33,7 +33,7 @@ |
255 | on with the registration process. |
256 | |
257 | >>> browser.getControl('E-mail address:', index=1).value = 'mpo@iki.fi' |
258 | - >>> set_captcha_answer(browser) |
259 | + >>> set_captcha_answer(browser, prefix='loginpage_') |
260 | >>> browser.getControl('Register').click() |
261 | |
262 | >>> print extract_text(find_tag_by_id(browser.contents, 'address')) |
263 | @@ -85,7 +85,7 @@ |
264 | |
265 | >>> anon_browser.getControl('E-mail address:', index=1).value = ( |
266 | ... 'christian.reis@ubuntulinux.com') |
267 | - >>> set_captcha_answer(anon_browser) |
268 | + >>> set_captcha_answer(anon_browser, prefix='loginpage_') |
269 | >>> anon_browser.getControl('Register').click() |
270 | |
271 | >>> from_addr, to_addr, msg = stub.test_emails.pop() |
272 | @@ -147,7 +147,7 @@ |
273 | |
274 | >>> browser.getControl('E-mail address:', index=1).value = ( |
275 | ... 'bad-user@canonical.com') |
276 | - >>> set_captcha_answer(browser) |
277 | + >>> set_captcha_answer(browser, prefix='loginpage_') |
278 | >>> browser.getControl('Register').click() |
279 | |
280 | >>> print extract_text(find_main_content(browser.contents).p) |
281 | |
282 | === modified file 'lib/lp/registry/stories/foaf/xx-resetpassword.txt' |
283 | --- lib/lp/registry/stories/foaf/xx-resetpassword.txt 2009-09-18 01:09:10 +0000 |
284 | +++ lib/lp/registry/stories/foaf/xx-resetpassword.txt 2009-10-16 19:59:17 +0000 |
285 | @@ -15,10 +15,12 @@ |
286 | >>> browser.title |
287 | 'Need a new Launchpad password?' |
288 | |
289 | -He types the email address registered in Launchpad and submit the form. |
290 | +He types the email address registered in Launchpad and submits the form. |
291 | |
292 | + >>> from lp.testing.registration import set_captcha_answer |
293 | >>> browser.getControl(name='email').value = ( |
294 | ... 'david.allouche@canonical.com') |
295 | + >>> set_captcha_answer(browser) |
296 | >>> browser.getControl('Request Reset').click() |
297 | >>> print extract_text(find_main_content(browser.contents)) |
298 | Need a new Launchpad password? |
299 | @@ -68,8 +70,7 @@ |
300 | >>> browser.url |
301 | 'http://launchpad.dev/token/.../+resetpassword' |
302 | |
303 | - >>> for tag in get_feedback_messages(browser.contents): |
304 | - ... print extract_text(tag) |
305 | + >>> print_feedback_messages(browser.contents) |
306 | There is 1 error. |
307 | The password provided contains non-ASCII characters. |
308 | |
309 | @@ -82,8 +83,7 @@ |
310 | >>> browser.url |
311 | 'http://launchpad.dev' |
312 | |
313 | - >>> for tag in get_feedback_messages(browser.contents): |
314 | - ... print extract_text(tag) |
315 | + >>> print_feedback_messages(browser.contents) |
316 | Your password has been reset successfully. |
317 | |
318 | >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol')) |
319 | @@ -104,8 +104,7 @@ |
320 | logs out and then logs in again: |
321 | |
322 | >>> browser.open('http://launchpad.dev/+logout') |
323 | - >>> for tag in get_feedback_messages(browser.contents): |
324 | - ... print extract_text(tag) |
325 | + >>> print_feedback_messages(browser.contents) |
326 | You have been logged out |
327 | |
328 | >>> browser.getLink('Log in / Register').click() |
329 | @@ -122,6 +121,20 @@ |
330 | David Allouche... |
331 | |
332 | |
333 | +== Reset is protected by a math captcha == |
334 | + |
335 | +When requesting a password reset David is presented with a simple math |
336 | +problem which must be solved correctly before proceeding. |
337 | + |
338 | + >>> browser.open('http://launchpad.dev/+forgottenpassword') |
339 | + >>> browser.getControl(name='email').value = ( |
340 | + ... 'david.allouche@canonical.com') |
341 | + >>> browser.getControl(name='captcha_submission').value = '-1' |
342 | + >>> browser.getControl('Request Reset').click() |
343 | + >>> print_feedback_messages(browser.contents) |
344 | + The answer to the simple math question was incorrect or missing. Please try again. |
345 | + |
346 | + |
347 | == Using email addresses other than the preferred one == |
348 | |
349 | Any of a person's validated email addresses can be used to reset his |
350 | @@ -134,8 +147,9 @@ |
351 | >>> browser.open('http://launchpad.dev/+login') |
352 | >>> browser.getLink('Forgotten your password?').click() |
353 | >>> browser.getControl(name='email').value = 'david@canonical.com' |
354 | + >>> set_captcha_answer(browser) |
355 | >>> browser.getControl('Request Reset').click() |
356 | - >>> print "\n".join(get_feedback_messages(browser.contents)) |
357 | + >>> print_feedback_messages(browser.contents) |
358 | |
359 | He follows the link sent to his email just like he did before. |
360 | |
361 | @@ -155,7 +169,7 @@ |
362 | >>> browser.getControl('Continue').click() |
363 | >>> browser.url |
364 | 'http://launchpad.dev' |
365 | - >>> print "\n".join(get_feedback_messages(browser.contents)) |
366 | + >>> print_feedback_messages(browser.contents) |
367 | Your password has been reset successfully. |
368 | |
369 | |
370 | @@ -165,12 +179,11 @@ |
371 | |
372 | >>> browser.open('http://launchpad.dev/+forgottenpassword') |
373 | >>> browser.getControl(name='email').value = 'support@ubuntu.com' |
374 | + >>> set_captcha_answer(browser) |
375 | >>> browser.getControl('Request Reset').click() |
376 | - |
377 | - >>> for tag in find_tags_by_class(browser.contents, 'error'): |
378 | - ... print tag |
379 | - <div class="error">The email address <strong>support@ubuntu.com</strong> |
380 | - belongs to a team, and teams cannot log in to Launchpad.</div> |
381 | + >>> print_feedback_messages(browser.contents) |
382 | + The email address support@ubuntu.com |
383 | + belongs to a team, and teams cannot log in to Launchpad. |
384 | |
385 | |
386 | == Reactivating an account == |
387 | @@ -202,8 +215,7 @@ |
388 | >>> browser.title |
389 | 'Log in or register with Launchpad' |
390 | |
391 | - >>> for tag in get_feedback_messages(browser.contents): |
392 | - ... print extract_text(tag) |
393 | + >>> print_feedback_messages(browser.contents) |
394 | The email address belongs to a deactivated account. Use the |
395 | "Forgotten your password" link to reactivate it. |
396 | |
397 | @@ -215,6 +227,7 @@ |
398 | |
399 | >>> browser.getControl(name='email').value = ( |
400 | ... 'former-user@canonical.com') |
401 | + >>> set_captcha_answer(browser) |
402 | >>> browser.getControl('Request Reset').click() |
403 | |
404 | >>> print extract_text(find_main_content(browser.contents).p) |
405 | @@ -248,8 +261,7 @@ |
406 | >>> browser.url |
407 | 'http://launchpad.dev' |
408 | |
409 | - >>> for tag in get_feedback_messages(browser.contents): |
410 | - ... print extract_text(tag) |
411 | + >>> print_feedback_messages(browser.contents) |
412 | Welcome back to Launchpad. |
413 | Your password has been reset successfully. |
414 | |
415 | @@ -300,6 +312,7 @@ |
416 | 'Need a new Launchpad password?' |
417 | >>> browser.getControl(name='email').value = ( |
418 | ... 'bad-user@canonical.com') |
419 | + >>> set_captcha_answer(browser) |
420 | >>> browser.getControl('Request Reset').click() |
421 | |
422 | >>> print extract_text(find_main_content(browser.contents).p) |
423 | @@ -330,9 +343,6 @@ |
424 | >>> browser.getControl(name='field.password_dupe').value = 'test' |
425 | >>> browser.getControl('Continue').click() |
426 | |
427 | - >>> for tag in find_tags_by_class( |
428 | - ... browser.contents, 'warning'): |
429 | - ... print tag |
430 | - <div ...>Your password cannot be reset because your account is suspended. |
431 | - Contact a <a href="mailto:feedback@launchpad.net?subject=SU...">Launchpad |
432 | - admin</a> about this issue.</div> |
433 | + >>> print_feedback_messages(browser.contents) |
434 | + Your password cannot be reset because your account is suspended. |
435 | + Contact a Launchpad admin about this issue. |
436 | |
437 | === modified file 'lib/lp/testing/registration.py' |
438 | --- lib/lp/testing/registration.py 2009-10-08 20:22:25 +0000 |
439 | +++ lib/lp/testing/registration.py 2009-10-16 19:59:17 +0000 |
440 | @@ -24,9 +24,9 @@ |
441 | return '' |
442 | |
443 | |
444 | -def set_captcha_answer(browser, answer=None): |
445 | +def set_captcha_answer(browser, answer=None, prefix=''): |
446 | """Given a browser, set the login captcha with the correct answer.""" |
447 | if answer is None: |
448 | answer = get_captcha_answer(browser.contents) |
449 | - browser.getControl(name='loginpage_captcha_submission').value = ( |
450 | + browser.getControl(name=prefix + 'captcha_submission').value = ( |
451 | answer) |
= Summary =
Bug 452491 requests a simple captcha be placed on the forgotten password page.
== Proposed fix ==
The captcha already existed on the registration page. It was quick work to refactor
the bits into a mixin class and let the forgotten password page use it. Sadly both
of those pages are done without using LaunchpadFormView, made instead with grout and
twine, so it took a little massaging to get things to work.
The test helper set_captcha_answer had to be made a little smarter to account for
form prefixes.
The reset password test needed some clean up too. There is a fair amount of drive by
stuff in here but it's easy to sort out, I hope.
I also changed the spacing on the error messages for this page and the registration people. canonical. com/~bac/ captcha/
page to fix some overlap problems. Screenshots are at:
http://
== Pre-implementation notes ==
None
== Implementation details ==
As above.
== Tests ==
bin/test -vvm lp.registry -t foaf -t xx-new- account- redirection- url.txt
== Demo and Q/A ==
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: /launchpad/ templates/ launchpad- login.pt /launchpad/ templates/ launchpad- forgottenpasswo rd.pt registry/ stories/ foaf/xx- reg-with- existing- email.txt registry/ stories/ foaf/xx- createaccount. txt /launchpad/ browser/ tests/registrat ion.py /launchpad/ webapp/ login.py testing/ registration. py /launchpad/ pagetests/ standalone/ xx-new- account- redirection- url.txt registry/ stories/ foaf/xx- resetpassword. txt
lib/canonical
lib/canonical
lib/lp/
lib/lp/
lib/canonical
lib/canonical
lib/lp/
lib/canonical
lib/lp/