Merge lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications into lp:launchpad/db-devel
- add-date-last-changed-who-changed-to-specifications
- Merge into db-devel
Proposed by
Ursula Junque
Status: | Merged |
---|---|
Approved by: | Stuart Bishop |
Approved revision: | no longer in the source branch. |
Merged at revision: | 11298 |
Proposed branch: | lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
3284 lines (+702/-1593) 45 files modified
buildout.cfg (+1/-1) database/schema/patch-2209-04-0.sql (+17/-0) lib/canonical/launchpad/icing/css/base.css (+3/-0) lib/canonical/launchpad/icing/css/components/bug_listing.css (+3/-6) lib/lp/app/javascript/comment.js (+17/-15) lib/lp/app/javascript/extras/extras.js (+0/-2) lib/lp/app/javascript/foldables.js (+0/-1) lib/lp/app/javascript/sorttable/sorttable.js (+467/-307) lib/lp/app/javascript/tests/test_foldables.js (+0/-1) lib/lp/app/templates/base-layout-macros.pt (+4/-3) lib/lp/archiveuploader/tests/nascentuploadfile.txt (+0/-31) lib/lp/bugs/browser/tests/test_bugtask.py (+1/-1) lib/lp/bugs/doc/externalbugtracker-comment-imports.txt (+0/-17) lib/lp/bugs/scripts/bugimport.py (+3/-22) lib/lp/bugs/scripts/tests/test_bugimport.py (+0/-28) lib/lp/bugs/templates/buglisting.mustache (+54/-54) lib/lp/registry/doc/person-account.txt (+5/-14) lib/lp/registry/doc/person.txt (+1/-7) lib/lp/registry/model/person.py (+51/-111) lib/lp/registry/tests/test_mailinglistapi.py (+0/-4) lib/lp/registry/tests/test_personset.py (+0/-81) lib/lp/registry/xmlrpc/mailinglist.py (+0/-1) lib/lp/scripts/garbo.py (+1/-1) lib/lp/security.py (+30/-11) lib/lp/services/identity/doc/account.txt (+4/-160) lib/lp/services/identity/interfaces/account.py (+1/-102) lib/lp/services/identity/interfaces/emailaddress.py (+2/-7) lib/lp/services/identity/model/account.py (+1/-165) lib/lp/services/identity/model/emailaddress.py (+3/-22) lib/lp/services/identity/tests/test_account.py (+1/-241) lib/lp/services/mail/incoming.py (+0/-6) lib/lp/services/mail/tests/test_incoming.py (+0/-7) lib/lp/services/verification/browser/logintoken.py (+2/-11) lib/lp/services/webapp/authentication.py (+6/-8) lib/lp/services/webapp/login.py (+3/-3) lib/lp/services/webapp/publication.py (+4/-3) lib/lp/services/webapp/tests/test_authentication.py (+1/-37) lib/lp/services/webapp/tests/test_authutility.py (+2/-2) lib/lp/services/webapp/tests/test_login.py (+4/-23) lib/lp/services/webapp/tests/test_login_account.py (+0/-33) lib/lp/services/webservice/configuration.py (+1/-1) lib/lp/testing/factory.py (+6/-19) lib/lp/testing/tests/test_login.py (+0/-7) lib/lp/testopenid/browser/server.py (+3/-2) lib/lp/translations/utilities/tests/test_file_importer.py (+0/-15) |
To merge this branch: | bzr merge lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | db | Approve | |
Robert Collins | db | Pending | |
Launchpad code reviewers | Pending | ||
Review via email: mp+88503@code.launchpad.net |
Commit message
[r=stub][bug=916043][incr] Add date_last_changed and last_changed_by to specification.
Description of the change
This branch adds two new columns to table specification: date_last_changed and last_changed_by. These two should be updated whenever one updates a blueprint, so it's possible to know, well, when the last change was made and who did it.
To post a comment you must log in.
Revision history for this message
Ursula Junque (ursinha) wrote : | # |
> Looks good.
>
> I'm told we want to order specifications by last update time, so we will need
> an index on that column.
Done!
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'buildout.cfg' |
2 | --- buildout.cfg 2011-12-30 06:47:17 +0000 |
3 | +++ buildout.cfg 2012-01-13 14:12:26 +0000 |
4 | @@ -43,7 +43,7 @@ |
5 | rm -rf ${buildout:yui-directory}/yui-${versions:yui}/* |
6 | tar -zxf download-cache/dist/yui-${versions:yui}.tar.gz \ |
7 | -C ${buildout:yui-directory}/yui-${versions:yui} |
8 | - ln -s ../../../../${buildout:yui-directory}/yui-${versions:yui} \ |
9 | + ln -sf ../../../../${buildout:yui-directory}/yui-${versions:yui} \ |
10 | lib/canonical/launchpad/icing/yui |
11 | |
12 | [filetemplates] |
13 | |
14 | === added file 'database/schema/patch-2209-04-0.sql' |
15 | --- database/schema/patch-2209-04-0.sql 1970-01-01 00:00:00 +0000 |
16 | +++ database/schema/patch-2209-04-0.sql 2012-01-13 14:12:26 +0000 |
17 | @@ -0,0 +1,17 @@ |
18 | +-- Copyright 2011 Canonical Ltd. This software is licensed under the |
19 | +-- GNU Affero General Public License version 3 (see the file LICENSE). |
20 | + |
21 | +SET client_min_messages=ERROR; |
22 | + |
23 | +ALTER TABLE specification |
24 | + ADD COLUMN date_last_changed timestamp without time zone, |
25 | + ADD COLUMN last_changed_by integer REFERENCES person; |
26 | + |
27 | +ALTER TABLE specification ALTER COLUMN date_last_changed SET DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'); |
28 | + |
29 | +CREATE INDEX specification__last_changed_by__idx ON specification USING btree (last_changed_by) WHERE (last_changed_by IS NOT NULL); |
30 | +CREATE INDEX specification__date_last_changed__idx ON specification USING btree (date_last_changed); |
31 | + |
32 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 04, 0); |
33 | + |
34 | + |
35 | |
36 | === modified file 'lib/canonical/launchpad/icing/css/base.css' |
37 | --- lib/canonical/launchpad/icing/css/base.css 2011-12-07 04:57:36 +0000 |
38 | +++ lib/canonical/launchpad/icing/css/base.css 2012-01-13 14:12:26 +0000 |
39 | @@ -264,6 +264,9 @@ |
40 | table.sortable img.sortarrow { |
41 | padding-left: 2px; |
42 | } |
43 | +table.sortable th { |
44 | + cursor: pointer; |
45 | + } |
46 | th.ascending { |
47 | background-image: url(/@@/arrowDown); |
48 | background-position: center right; |
49 | |
50 | === modified file 'lib/canonical/launchpad/icing/css/components/bug_listing.css' |
51 | --- lib/canonical/launchpad/icing/css/components/bug_listing.css 2011-12-21 06:19:51 +0000 |
52 | +++ lib/canonical/launchpad/icing/css/components/bug_listing.css 2012-01-13 14:12:26 +0000 |
53 | @@ -15,13 +15,10 @@ |
54 | float: left; |
55 | padding-right: 10px; |
56 | } |
57 | -div.buglisting-col2, |
58 | -div.buglisting-col3 { |
59 | +div.buglisting-col2 { |
60 | float: left; |
61 | margin-top: 2px; |
62 | - } |
63 | -div.buglisting-col2 { |
64 | - width: 40%; |
65 | + width: 68%; |
66 | } |
67 | div#client-listing .status, |
68 | div#client-listing .importance { |
69 | @@ -33,7 +30,7 @@ |
70 | line-height: 12px; |
71 | text-align: center; |
72 | margin: 5px 10px 0 0; |
73 | -} |
74 | + } |
75 | div#client-listing .importance { |
76 | width: 65px; |
77 | } |
78 | |
79 | === modified file 'lib/lp/app/javascript/comment.js' |
80 | --- lib/lp/app/javascript/comment.js 2011-08-09 14:18:02 +0000 |
81 | +++ lib/lp/app/javascript/comment.js 2012-01-13 14:12:26 +0000 |
82 | @@ -20,11 +20,13 @@ |
83 | */ |
84 | initializer: function() { |
85 | this.submit_button = this.get_submit(); |
86 | - this.comment_input = Y.one('[id="field.comment"]'); |
87 | + this.comment_input = Y.one( |
88 | + 'div#add-comment-form [id="field.comment"]'); |
89 | this.lp_client = new Y.lp.client.Launchpad(); |
90 | this.error_handler = new Y.lp.client.ErrorHandler(); |
91 | - this.error_handler.clearProgressUI = bind(this.clearProgressUI, this); |
92 | - this.error_handler.showError = bind(function (error_msg) { |
93 | + this.error_handler.clearProgressUI = Y.bind( |
94 | + this.clearProgressUI, this); |
95 | + this.error_handler.showError = Y.bind(function (error_msg) { |
96 | Y.lp.app.errors.display_error(this.submit_button, error_msg); |
97 | }, this); |
98 | this.progress_message = Y.Node.create( |
99 | @@ -39,7 +41,7 @@ |
100 | * @method get_submit |
101 | */ |
102 | get_submit: function(){ |
103 | - return Y.one('[id="field.actions.save"]'); |
104 | + return Y.one('div#add-comment-form input[id="field.actions.save"]'); |
105 | }, |
106 | /** |
107 | * Implementation of Widget.renderUI. |
108 | @@ -86,9 +88,9 @@ |
109 | return; |
110 | } |
111 | this.activateProgressUI('Saving...'); |
112 | - this.post_comment(bind(function(message_entry) { |
113 | + this.post_comment(Y.bind(function(message_entry) { |
114 | this.get_comment_HTML( |
115 | - message_entry, bind(this.insert_comment_HTML, this)); |
116 | + message_entry, Y.bind(this.insert_comment_HTML, this)); |
117 | this._add_comment_success(); |
118 | }, this)); |
119 | }, |
120 | @@ -186,9 +188,9 @@ |
121 | * @method bindUI |
122 | */ |
123 | bindUI: function(){ |
124 | - this.comment_input.on('keyup', bind(this.syncUI, this)); |
125 | - this.comment_input.on('mouseup', bind(this.syncUI, this)); |
126 | - this.submit_button.on('click', bind(this.add_comment, this)); |
127 | + this.comment_input.on('keyup', this.syncUI, this); |
128 | + this.comment_input.on('mouseup', this.syncUI, this); |
129 | + this.submit_button.on('click', this.add_comment, this); |
130 | }, |
131 | /** |
132 | * Implementation of Widget.syncUI: Update appearance according to state. |
133 | @@ -325,7 +327,7 @@ |
134 | window.scrollTo(0, Y.one('#add-comment').getY()); |
135 | this.lp_client.get(object_url, { |
136 | on: { |
137 | - success: bind(function(comment){ |
138 | + success: Y.bind(function(comment){ |
139 | this.set_in_reply_to(comment); |
140 | this.clearProgressUI(); |
141 | this.syncUI(); |
142 | @@ -380,11 +382,11 @@ |
143 | */ |
144 | bindUI: function() { |
145 | CodeReviewComment.superclass.bindUI.apply(this); |
146 | - this.vote_input.on('keyup', bind(this.syncUI, this)); |
147 | - this.vote_input.on('change', bind(this.syncUI, this)); |
148 | - this.review_type.on('keyup', bind(this.syncUI, this)); |
149 | - this.review_type.on('mouseup', bind(this.syncUI, this)); |
150 | - Y.all('a.menu-link-reply').on('click', bind(this.reply_clicked, this)); |
151 | + this.vote_input.on('keyup', this.syncUI, this); |
152 | + this.vote_input.on('change', this.syncUI, this); |
153 | + this.review_type.on('keyup', this.syncUI, this); |
154 | + this.review_type.on('mouseup', this.syncUI, this); |
155 | + Y.all('a.menu-link-reply').on('click', this.reply_clicked, this); |
156 | }, |
157 | /** |
158 | * Implementation of Widget.syncUI: Update appearance according to state. |
159 | |
160 | === modified file 'lib/lp/app/javascript/extras/extras.js' |
161 | --- lib/lp/app/javascript/extras/extras.js 2011-08-05 09:23:53 +0000 |
162 | +++ lib/lp/app/javascript/extras/extras.js 2012-01-13 14:12:26 +0000 |
163 | @@ -10,8 +10,6 @@ |
164 | |
165 | YUI.add('lp.extras', function(Y) { |
166 | |
167 | -Y.log('loading lp.extras'); |
168 | - |
169 | var namespace = Y.namespace("lp.extras"), |
170 | NodeList = Y.NodeList; |
171 | |
172 | |
173 | === modified file 'lib/lp/app/javascript/foldables.js' |
174 | --- lib/lp/app/javascript/foldables.js 2012-01-10 17:23:23 +0000 |
175 | +++ lib/lp/app/javascript/foldables.js 2012-01-13 14:12:26 +0000 |
176 | @@ -46,7 +46,6 @@ |
177 | included.each(function (span, index, list) { |
178 | if (span.hasClass('foldable-quoted')) { |
179 | var quoted_lines = span.all('br'); |
180 | - debugger; |
181 | if (quoted_lines && quoted_lines.size() <= 11) { |
182 | // We do not hide short quoted passages (12 lines) by |
183 | // default. |
184 | |
185 | === modified file 'lib/lp/app/javascript/sorttable/sorttable.js' |
186 | --- lib/lp/app/javascript/sorttable/sorttable.js 2009-06-03 14:52:54 +0000 |
187 | +++ lib/lp/app/javascript/sorttable/sorttable.js 2012-01-13 14:12:26 +0000 |
188 | @@ -1,309 +1,469 @@ |
189 | -/* |
190 | - * Table sorting Javascript. MIT-licensed. Originally from |
191 | - * |
192 | - * http://www.kryogenix.org/code/browser/sorttable/ |
193 | - * |
194 | - * 2005-07-13: Initial import. Changed the arrow code to use the images |
195 | - * included with Plone instead of the entities. Also changed |
196 | - * the way sorting of a previously-sorted column behaves |
197 | - * slightly. Reformatted to try keeping to 80 columns. (kiko) |
198 | - * |
199 | - * 2006-03-13: Added support for indicating sortkeys (direct and |
200 | - * reverse) inside table cells. Removed tabs. (kiko) |
201 | - * |
202 | - * 2006-04-08: Fixed matching of "sortable". Added support for "initial-sort". |
203 | - * Made sorting stable. (ddaa) |
204 | - * |
205 | - * 2006-04-11: Fixed numeric sorting to be robust in the presence of |
206 | - * whitespace; added trim(). Note that parseFloat() deals |
207 | - * with leading and trailing whitespace just fine. (kiko) |
208 | - * |
209 | - * 2006-04-12: Added default-sort and default-revsort classes. When |
210 | - * the data in the table is already pre-sorted, you can use |
211 | - * these classes to indicate by which column they were |
212 | - * ordered by. This, in turn, allows us to correctly |
213 | - * Javascript-sort them later. (kiko) |
214 | - * |
215 | - * 2006-10-15: Moved the window load event to main-template.pt, so that this |
216 | - * script's load event doesn't stomp those of other scripts. (mpt) |
217 | +/* |
218 | + SortTable |
219 | + version 2 |
220 | + 7th April 2007 |
221 | + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ |
222 | + |
223 | + Instructions: |
224 | + Download this file |
225 | + Add <script src="sorttable.js"></script> to your HTML |
226 | + Add class="sortable" to any table you'd like to make sortable |
227 | + Click on the headers to sort |
228 | + |
229 | + Thanks to many, many people for contributions and suggestions. |
230 | + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html |
231 | + This basically means: do what you want with it. |
232 | +*/ |
233 | + |
234 | +/** |
235 | + * New api is: |
236 | + * var sortable = new Y.lp.app.sortable.Sortable(); |
237 | + * sortable.init(); |
238 | */ |
239 | |
240 | -var SORT_COLUMN_INDEX; |
241 | - |
242 | -var arrowUp = "/@@/arrowUp"; |
243 | -var arrowDown = "/@@/arrowDown"; |
244 | -var arrowBlank = "/@@/arrowBlank"; |
245 | - |
246 | -function trim(str) { |
247 | - return str.replace(/^\s*|\s*$/g, ""); |
248 | -} |
249 | - |
250 | -function sortables_init() { |
251 | - // Find all tables with class sortable and make them sortable |
252 | - if (!document.getElementsByTagName) return; |
253 | - tbls = document.getElementsByTagName("table"); |
254 | - for (ti=0;ti<tbls.length;ti++) { |
255 | - thisTbl = tbls[ti]; |
256 | - if (((' '+thisTbl.className+' ').indexOf(" sortable ") != -1) && |
257 | - (thisTbl.id)) { |
258 | - ts_makeSortable(thisTbl); |
259 | - } |
260 | - } |
261 | -} |
262 | - |
263 | -function ts_makeSortable(table) { |
264 | - if (table.tHead && table.tHead.rows && table.tHead.rows.length > 0) { |
265 | - var firstRow = table.tHead.rows[0]; |
266 | - } else if (table.rows && table.rows.length > 0) { |
267 | - var firstRow = table.rows[0]; |
268 | - } |
269 | - if (!firstRow) return; |
270 | - |
271 | - // We have a first row: assume it's the header, and make its |
272 | - // contents clickable links |
273 | - for (var i=0;i<firstRow.cells.length;i++) { |
274 | - var cell = firstRow.cells[i]; |
275 | - var txt = ts_getInnerText(cell); |
276 | - cell.innerHTML = '<a href="#" class="sortheader" onclick="ts_resortTable(this); return false;">' |
277 | - + txt + |
278 | - '<img class="sortarrow" src="'+arrowBlank+'" height="6" width="9"></a>'; |
279 | - } |
280 | - |
281 | - // Sort by the first column whose title cell has initial-sort class. |
282 | - for (var i=0; i<firstRow.cells.length; i++) { |
283 | - var cell = firstRow.cells[i]; |
284 | - var lnk = ts_firstChildByName(cell, 'A'); |
285 | - var img = ts_firstChildByName(lnk, 'IMG') |
286 | - if ((' ' + cell.className + ' ').indexOf(" default-sort ") != -1) { |
287 | - ts_arrowDown(img); |
288 | - } |
289 | - if ((' ' + cell.className + ' ').indexOf(" default-revsort ") != -1) { |
290 | - ts_arrowUp(img); |
291 | - } |
292 | - if ((' ' + cell.className + ' ').indexOf(" initial-sort ") != -1) { |
293 | - ts_resortTable(lnk); |
294 | - } |
295 | - } |
296 | -} |
297 | - |
298 | -function ts_getInnerText(el) { |
299 | - if (typeof el == "string") return el; |
300 | - if (typeof el == "undefined") { return el }; |
301 | - if (el.innerText) return el.innerText; //Not needed but it is faster |
302 | - var str = ""; |
303 | - |
304 | - var cs = el.childNodes; |
305 | - var l = cs.length; |
306 | - for (var i = 0; i < l; i++) { |
307 | - node = cs[i]; |
308 | - switch (node.nodeType) { |
309 | - case 1: //ELEMENT_NODE |
310 | - if (node.className == "sortkey") { |
311 | - return ts_getInnerText(node); |
312 | - } else if (node.className == "revsortkey") { |
313 | - return "-" + ts_getInnerText(node); |
314 | - } else { |
315 | - str += ts_getInnerText(node); |
316 | - break; |
317 | - } |
318 | - case 3: //TEXT_NODE |
319 | - str += node.nodeValue; |
320 | - break; |
321 | - } |
322 | - } |
323 | - return str; |
324 | -} |
325 | - |
326 | -function ts_firstChildByName(el, name) { |
327 | - for (var ci=0; ci < el.childNodes.length; ci++) { |
328 | - if (el.childNodes[ci].tagName && |
329 | - el.childNodes[ci].tagName.toLowerCase() == name.toLowerCase()) |
330 | - return el.childNodes[ci]; |
331 | - } |
332 | -} |
333 | - |
334 | -function ts_arrowUp(img) { |
335 | - img.setAttribute('sortdir','up'); |
336 | - img.src = arrowUp; |
337 | -} |
338 | - |
339 | -function ts_arrowDown(img) { |
340 | - img.setAttribute('sortdir','down'); |
341 | - img.src = arrowDown; |
342 | -} |
343 | - |
344 | -function ts_resortTable(lnk) { |
345 | - // get the img |
346 | - var img = ts_firstChildByName(lnk, 'IMG') |
347 | - var td = lnk.parentNode; |
348 | - var column = td.cellIndex; |
349 | - var table = getParent(td,'TABLE'); |
350 | - |
351 | - if (table.rows.length <= 1) return; |
352 | - |
353 | - SORT_COLUMN_INDEX = column; |
354 | - // If some previous column contains a colspan, we need to increase |
355 | - // the column index to compensate for it. |
356 | - while (td.previousSibling != null) { |
357 | - td = td.previousSibling; |
358 | - if (td.nodeType != 1) { |
359 | - continue |
360 | - } |
361 | - colspan = td.getAttribute("colspan"); |
362 | - if (colspan) { |
363 | - SORT_COLUMN_INDEX += parseInt(colspan) - 1; |
364 | - } |
365 | - } |
366 | - |
367 | - // Work out a type for the column |
368 | - var itm = ts_getInnerText(table.rows[1].cells[SORT_COLUMN_INDEX]); |
369 | - itm = trim(itm); |
370 | - |
371 | - sortfn = ts_sort_caseinsensitive; |
372 | - if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date; |
373 | - if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date; |
374 | - if (itm.match(/^[£$]/)) sortfn = ts_sort_currency; |
375 | - if (itm.match(/^-?[\d\.]+$/)) sortfn = ts_sort_numeric; |
376 | - |
377 | - var firstRow = new Array(); |
378 | - var newRows = new Array(); |
379 | - for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; } |
380 | - for (j=1;j<table.rows.length;j++) { |
381 | - newRows[j-1] = table.rows[j]; |
382 | - newRows[j-1].oldPosition = j-1; |
383 | - } |
384 | - |
385 | - newRows.sort(ts_stableSort(sortfn)); |
386 | - |
387 | - if (img.getAttribute("sortdir") == 'down') { |
388 | - newRows.reverse(); |
389 | - ts_arrowUp(img); |
390 | - } else { |
391 | - ts_arrowDown(img); |
392 | - } |
393 | - |
394 | - // We appendChild rows that already exist to the tbody, so it moves |
395 | - // them rather than creating new ones |
396 | - for (i=0;i<newRows.length;i++) { |
397 | - if (!newRows[i].className || |
398 | - (newRows[i].className && |
399 | - (newRows[i].className.indexOf('sortbottom') == -1))) |
400 | - // don't do sortbottom rows |
401 | - table.tBodies[0].appendChild(newRows[i]); |
402 | - } |
403 | - // do sortbottom rows only |
404 | - for (i=0;i<newRows.length;i++) { |
405 | - if (newRows[i].className && |
406 | - (newRows[i].className.indexOf('sortbottom') != -1)) |
407 | - table.tBodies[0].appendChild(newRows[i]); |
408 | - } |
409 | - |
410 | - // Delete any other arrows there may be showing |
411 | - var allimgs = document.getElementsByTagName("img"); |
412 | - for (var ci=0; ci<allimgs.length; ci++) { |
413 | - var one_img = allimgs[ci]; |
414 | - if (one_img != img && |
415 | - one_img.className == 'sortarrow' && |
416 | - getParent(one_img, "table") == getParent(lnk, "table")) { |
417 | - one_img.src = arrowBlank; |
418 | - one_img.setAttribute('sortdir', ''); |
419 | - } |
420 | - } |
421 | -} |
422 | - |
423 | -function getParent(el, pTagName) { |
424 | - if (el == null) |
425 | - return null; |
426 | - else if (el.nodeType == 1 && |
427 | - el.tagName.toLowerCase() == pTagName.toLowerCase()) |
428 | - // Gecko bug, supposed to be uppercase |
429 | - return el; |
430 | - else |
431 | - return getParent(el.parentNode, pTagName); |
432 | -} |
433 | - |
434 | -function ts_stableSort(sortfn) { |
435 | - // Return a comparison function based on sortfn, but using oldPosition |
436 | - // attributes to discriminate between objects that sortfn compares as |
437 | - // equal, effectively providing stable sort. |
438 | - function stableSort(a, b) { |
439 | - var cmp = sortfn(a, b); |
440 | - if (cmp != 0) { |
441 | - return cmp; |
442 | - } else { |
443 | - return a.oldPosition - b.oldPosition; |
444 | - } |
445 | - } |
446 | - return stableSort; |
447 | -} |
448 | - |
449 | -function ts_sort_date(a,b) { |
450 | - // y2k notes: two digit years less than 50 are treated as 20XX, |
451 | - // greater than 50 are treated as 19XX |
452 | - aa = trim(ts_getInnerText(a.cells[SORT_COLUMN_INDEX])); |
453 | - bb = trim(ts_getInnerText(b.cells[SORT_COLUMN_INDEX])); |
454 | - if (aa.length == 10) { |
455 | - dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2); |
456 | - } else { |
457 | - yr = aa.substr(6,2); |
458 | - if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; } |
459 | - dt1 = yr+aa.substr(3,2)+aa.substr(0,2); |
460 | - } |
461 | - if (bb.length == 10) { |
462 | - dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2); |
463 | - } else { |
464 | - yr = bb.substr(6,2); |
465 | - if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; } |
466 | - dt2 = yr+bb.substr(3,2)+bb.substr(0,2); |
467 | - } |
468 | - if (dt1==dt2) return 0; |
469 | - if (dt1<dt2) return -1; |
470 | - return 1; |
471 | -} |
472 | - |
473 | -function ts_sort_currency(a,b) { |
474 | - aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,''); |
475 | - bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,''); |
476 | - return parseFloat(aa) - parseFloat(bb); |
477 | -} |
478 | - |
479 | -function ts_sort_numeric(a,b) { |
480 | - aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX])); |
481 | - if (isNaN(aa)) aa = 0; |
482 | - bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX])); |
483 | - if (isNaN(bb)) bb = 0; |
484 | - return aa-bb; |
485 | -} |
486 | - |
487 | -function ts_sort_caseinsensitive(a,b) { |
488 | - aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase(); |
489 | - bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase(); |
490 | - if (aa==bb) return 0; |
491 | - if (aa<bb) return -1; |
492 | - return 1; |
493 | -} |
494 | - |
495 | -function ts_sort_default(a,b) { |
496 | - aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]); |
497 | - bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]); |
498 | - if (aa==bb) return 0; |
499 | - if (aa<bb) return -1; |
500 | - return 1; |
501 | -} |
502 | - |
503 | - |
504 | -// addEvent and removeEvent |
505 | -// cross-browser event handling for IE5+, NS6 and Mozilla |
506 | -// By Scott Andrew |
507 | -function addEvent(elm, evType, fn, useCapture) { |
508 | - if (elm.addEventListener){ |
509 | - elm.addEventListener(evType, fn, useCapture); |
510 | - return true; |
511 | - } else if (elm.attachEvent){ |
512 | - var r = elm.attachEvent("on"+evType, fn); |
513 | - return r; |
514 | - } else { |
515 | - alert("Handler could not be removed"); |
516 | - } |
517 | -} |
518 | - |
519 | + |
520 | +YUI.add('lp.app.sorttable', function(Y) { |
521 | + |
522 | + var namespace = Y.namespace('lp.app.sorttable'); |
523 | + |
524 | + var stIsIE = /*@cc_on!@*/false; |
525 | + |
526 | + sorttable = { |
527 | + init: function() { |
528 | + // quit if this function has already been called |
529 | + if (arguments.callee.done) return; |
530 | + // flag this function so we don't do the same thing twice |
531 | + arguments.callee.done = true; |
532 | + |
533 | + if (!document.createElement || !document.getElementsByTagName) return; |
534 | + |
535 | + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; |
536 | + |
537 | + forEach(document.getElementsByTagName('table'), function(table) { |
538 | + if (table.className.search(/\bsortable\b/) != -1) { |
539 | + sorttable.makeSortable(table); |
540 | + } |
541 | + }); |
542 | + |
543 | + }, |
544 | + |
545 | + makeSortable: function(table) { |
546 | + if (table.getElementsByTagName('thead').length == 0) { |
547 | + // table doesn't have a tHead. Since it should have, create one and |
548 | + // put the first table row in it. |
549 | + the = document.createElement('thead'); |
550 | + the.appendChild(table.rows[0]); |
551 | + table.insertBefore(the,table.firstChild); |
552 | + } |
553 | + // Safari doesn't support table.tHead, sigh |
554 | + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; |
555 | + |
556 | + if (table.tHead.rows.length != 1) return; // can't cope with two header rows |
557 | + |
558 | + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as |
559 | + // "total" rows, for example). This is B&R, since what you're supposed |
560 | + // to do is put them in a tfoot. So, if there are sortbottom rows, |
561 | + // for backwards compatibility, move them to tfoot (creating it if needed). |
562 | + sortbottomrows = []; |
563 | + for (var i=0; i<table.rows.length; i++) { |
564 | + if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { |
565 | + sortbottomrows[sortbottomrows.length] = table.rows[i]; |
566 | + } |
567 | + } |
568 | + if (sortbottomrows) { |
569 | + if (table.tFoot == null) { |
570 | + // table doesn't have a tfoot. Create one. |
571 | + tfo = document.createElement('tfoot'); |
572 | + table.appendChild(tfo); |
573 | + } |
574 | + for (var i=0; i<sortbottomrows.length; i++) { |
575 | + tfo.appendChild(sortbottomrows[i]); |
576 | + } |
577 | + delete sortbottomrows; |
578 | + } |
579 | + |
580 | + // work through each column and calculate its type |
581 | + headrow = table.tHead.rows[0].cells; |
582 | + for (var i=0; i<headrow.length; i++) { |
583 | + // manually override the type with a sorttable_type attribute |
584 | + if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col |
585 | + mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); |
586 | + if (mtch) { override = mtch[1]; } |
587 | + if (mtch && typeof sorttable["sort_"+override] == 'function') { |
588 | + headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; |
589 | + } else { |
590 | + headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); |
591 | + } |
592 | + // make it clickable to sort |
593 | + headrow[i].sorttable_columnindex = i; |
594 | + headrow[i].sorttable_tbody = table.tBodies[0]; |
595 | + dean_addEvent(headrow[i],"click", function(e) { |
596 | + |
597 | + if (this.className.search(/\bsorttable_sorted\b/) != -1) { |
598 | + // if we're already sorted by this column, just |
599 | + // reverse the table, which is quicker |
600 | + sorttable.reverse(this.sorttable_tbody); |
601 | + this.className = this.className.replace('sorttable_sorted', |
602 | + 'sorttable_sorted_reverse'); |
603 | + this.removeChild(document.getElementById('sorttable_sortfwdind')); |
604 | + sortrevind = document.createElement('span'); |
605 | + sortrevind.id = "sorttable_sortrevind"; |
606 | + sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴'; |
607 | + this.appendChild(sortrevind); |
608 | + return; |
609 | + } |
610 | + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { |
611 | + // if we're already sorted by this column in reverse, just |
612 | + // re-reverse the table, which is quicker |
613 | + sorttable.reverse(this.sorttable_tbody); |
614 | + this.className = this.className.replace('sorttable_sorted_reverse', |
615 | + 'sorttable_sorted'); |
616 | + this.removeChild(document.getElementById('sorttable_sortrevind')); |
617 | + sortfwdind = document.createElement('span'); |
618 | + sortfwdind.id = "sorttable_sortfwdind"; |
619 | + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; |
620 | + this.appendChild(sortfwdind); |
621 | + return; |
622 | + } |
623 | + |
624 | + // remove sorttable_sorted classes |
625 | + theadrow = this.parentNode; |
626 | + forEach(theadrow.childNodes, function(cell) { |
627 | + if (cell.nodeType == 1) { // an element |
628 | + cell.className = cell.className.replace('sorttable_sorted_reverse',''); |
629 | + cell.className = cell.className.replace('sorttable_sorted',''); |
630 | + } |
631 | + }); |
632 | + sortfwdind = document.getElementById('sorttable_sortfwdind'); |
633 | + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } |
634 | + sortrevind = document.getElementById('sorttable_sortrevind'); |
635 | + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } |
636 | + |
637 | + this.className += ' sorttable_sorted'; |
638 | + sortfwdind = document.createElement('span'); |
639 | + sortfwdind.id = "sorttable_sortfwdind"; |
640 | + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; |
641 | + this.appendChild(sortfwdind); |
642 | + |
643 | + // build an array to sort. This is a Schwartzian transform thing, |
644 | + // i.e., we "decorate" each row with the actual sort key, |
645 | + // sort based on the sort keys, and then put the rows back in order |
646 | + // which is a lot faster because you only do getInnerText once per row |
647 | + row_array = []; |
648 | + col = this.sorttable_columnindex; |
649 | + rows = this.sorttable_tbody.rows; |
650 | + for (var j=0; j<rows.length; j++) { |
651 | + row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]]; |
652 | + } |
653 | + /* If you want a stable sort, uncomment the following line */ |
654 | + //sorttable.shaker_sort(row_array, this.sorttable_sortfunction); |
655 | + /* and comment out this one */ |
656 | + row_array.sort(this.sorttable_sortfunction); |
657 | + |
658 | + tb = this.sorttable_tbody; |
659 | + for (var j=0; j<row_array.length; j++) { |
660 | + tb.appendChild(row_array[j][1]); |
661 | + } |
662 | + |
663 | + delete row_array; |
664 | + }); |
665 | + } |
666 | + } |
667 | + }, |
668 | + |
669 | + guessType: function(table, column) { |
670 | + // guess the type of a column based on its first non-blank row |
671 | + sortfn = sorttable.sort_alpha; |
672 | + for (var i=0; i<table.tBodies[0].rows.length; i++) { |
673 | + text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); |
674 | + if (text != '') { |
675 | + if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { |
676 | + return sorttable.sort_numeric; |
677 | + } |
678 | + // check for a date: dd/mm/yyyy or dd/mm/yy |
679 | + // can have / or . or - as separator |
680 | + // can be mm/dd as well |
681 | + possdate = text.match(sorttable.DATE_RE) |
682 | + if (possdate) { |
683 | + // looks like a date |
684 | + first = parseInt(possdate[1]); |
685 | + second = parseInt(possdate[2]); |
686 | + if (first > 12) { |
687 | + // definitely dd/mm |
688 | + return sorttable.sort_ddmm; |
689 | + } else if (second > 12) { |
690 | + return sorttable.sort_mmdd; |
691 | + } else { |
692 | + // looks like a date, but we can't tell which, so assume |
693 | + // that it's dd/mm (English imperialism!) and keep looking |
694 | + sortfn = sorttable.sort_ddmm; |
695 | + } |
696 | + } |
697 | + } |
698 | + } |
699 | + return sortfn; |
700 | + }, |
701 | + |
702 | + getInnerText: function(node) { |
703 | + // gets the text we want to use for sorting for a cell. |
704 | + // strips leading and trailing whitespace. |
705 | + // this is *not* a generic getInnerText function; it's special to sorttable. |
706 | + // for example, you can override the cell text with a customkey attribute. |
707 | + // it also gets .value for <input> fields. |
708 | + |
709 | + hasInputs = (typeof node.getElementsByTagName == 'function') && |
710 | + node.getElementsByTagName('input').length; |
711 | + |
712 | + if (node.getAttribute("sorttable_customkey") != null) { |
713 | + return node.getAttribute("sorttable_customkey"); |
714 | + } |
715 | + else if (typeof node.textContent != 'undefined' && !hasInputs) { |
716 | + return node.textContent.replace(/^\s+|\s+$/g, ''); |
717 | + } |
718 | + else if (typeof node.innerText != 'undefined' && !hasInputs) { |
719 | + return node.innerText.replace(/^\s+|\s+$/g, ''); |
720 | + } |
721 | + else if (typeof node.text != 'undefined' && !hasInputs) { |
722 | + return node.text.replace(/^\s+|\s+$/g, ''); |
723 | + } |
724 | + else { |
725 | + switch (node.nodeType) { |
726 | + case 3: |
727 | + if (node.nodeName.toLowerCase() == 'input') { |
728 | + return node.value.replace(/^\s+|\s+$/g, ''); |
729 | + } |
730 | + case 4: |
731 | + return node.nodeValue.replace(/^\s+|\s+$/g, ''); |
732 | + break; |
733 | + case 1: |
734 | + case 11: |
735 | + var innerText = ''; |
736 | + for (var i = 0; i < node.childNodes.length; i++) { |
737 | + innerText += sorttable.getInnerText(node.childNodes[i]); |
738 | + } |
739 | + return innerText.replace(/^\s+|\s+$/g, ''); |
740 | + break; |
741 | + default: |
742 | + return ''; |
743 | + } |
744 | + } |
745 | + }, |
746 | + |
747 | + reverse: function(tbody) { |
748 | + // reverse the rows in a tbody |
749 | + newrows = []; |
750 | + for (var i=0; i<tbody.rows.length; i++) { |
751 | + newrows[newrows.length] = tbody.rows[i]; |
752 | + } |
753 | + for (var i=newrows.length-1; i>=0; i--) { |
754 | + tbody.appendChild(newrows[i]); |
755 | + } |
756 | + delete newrows; |
757 | + }, |
758 | + |
759 | + /* sort functions |
760 | + each sort function takes two parameters, a and b |
761 | + you are comparing a[0] and b[0] */ |
762 | + sort_numeric: function(a,b) { |
763 | + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); |
764 | + if (isNaN(aa)) aa = 0; |
765 | + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); |
766 | + if (isNaN(bb)) bb = 0; |
767 | + return aa-bb; |
768 | + }, |
769 | + sort_alpha: function(a,b) { |
770 | + if (a[0]==b[0]) return 0; |
771 | + if (a[0]<b[0]) return -1; |
772 | + return 1; |
773 | + }, |
774 | + sort_ddmm: function(a,b) { |
775 | + mtch = a[0].match(sorttable.DATE_RE); |
776 | + y = mtch[3]; m = mtch[2]; d = mtch[1]; |
777 | + if (m.length == 1) m = '0'+m; |
778 | + if (d.length == 1) d = '0'+d; |
779 | + dt1 = y+m+d; |
780 | + mtch = b[0].match(sorttable.DATE_RE); |
781 | + y = mtch[3]; m = mtch[2]; d = mtch[1]; |
782 | + if (m.length == 1) m = '0'+m; |
783 | + if (d.length == 1) d = '0'+d; |
784 | + dt2 = y+m+d; |
785 | + if (dt1==dt2) return 0; |
786 | + if (dt1<dt2) return -1; |
787 | + return 1; |
788 | + }, |
789 | + sort_mmdd: function(a,b) { |
790 | + mtch = a[0].match(sorttable.DATE_RE); |
791 | + y = mtch[3]; d = mtch[2]; m = mtch[1]; |
792 | + if (m.length == 1) m = '0'+m; |
793 | + if (d.length == 1) d = '0'+d; |
794 | + dt1 = y+m+d; |
795 | + mtch = b[0].match(sorttable.DATE_RE); |
796 | + y = mtch[3]; d = mtch[2]; m = mtch[1]; |
797 | + if (m.length == 1) m = '0'+m; |
798 | + if (d.length == 1) d = '0'+d; |
799 | + dt2 = y+m+d; |
800 | + if (dt1==dt2) return 0; |
801 | + if (dt1<dt2) return -1; |
802 | + return 1; |
803 | + }, |
804 | + |
805 | + shaker_sort: function(list, comp_func) { |
806 | + // A stable sort function to allow multi-level sorting of data |
807 | + // see: http://en.wikipedia.org/wiki/Cocktail_sort |
808 | + // thanks to Joseph Nahmias |
809 | + var b = 0; |
810 | + var t = list.length - 1; |
811 | + var swap = true; |
812 | + |
813 | + while(swap) { |
814 | + swap = false; |
815 | + for(var i = b; i < t; ++i) { |
816 | + if ( comp_func(list[i], list[i+1]) > 0 ) { |
817 | + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; |
818 | + swap = true; |
819 | + } |
820 | + } // for |
821 | + t--; |
822 | + |
823 | + if (!swap) break; |
824 | + |
825 | + for(var i = t; i > b; --i) { |
826 | + if ( comp_func(list[i], list[i-1]) < 0 ) { |
827 | + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; |
828 | + swap = true; |
829 | + } |
830 | + } // for |
831 | + b++; |
832 | + |
833 | + } // while(swap) |
834 | + } |
835 | + } |
836 | + |
837 | + // written by Dean Edwards, 2005 |
838 | + // with input from Tino Zijdel, Matthias Miller, Diego Perini |
839 | + |
840 | + // http://dean.edwards.name/weblog/2005/10/add-event/ |
841 | + |
842 | + function dean_addEvent(element, type, handler) { |
843 | + if (element.addEventListener) { |
844 | + element.addEventListener(type, handler, false); |
845 | + } else { |
846 | + // assign each event handler a unique ID |
847 | + if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; |
848 | + // create a hash table of event types for the element |
849 | + if (!element.events) element.events = {}; |
850 | + // create a hash table of event handlers for each element/event pair |
851 | + var handlers = element.events[type]; |
852 | + if (!handlers) { |
853 | + handlers = element.events[type] = {}; |
854 | + // store the existing event handler (if there is one) |
855 | + if (element["on" + type]) { |
856 | + handlers[0] = element["on" + type]; |
857 | + } |
858 | + } |
859 | + // store the event handler in the hash table |
860 | + handlers[handler.$$guid] = handler; |
861 | + // assign a global event handler to do all the work |
862 | + element["on" + type] = handleEvent; |
863 | + } |
864 | + }; |
865 | + // a counter used to create unique IDs |
866 | + dean_addEvent.guid = 1; |
867 | + |
868 | + function removeEvent(element, type, handler) { |
869 | + if (element.removeEventListener) { |
870 | + element.removeEventListener(type, handler, false); |
871 | + } else { |
872 | + // delete the event handler from the hash table |
873 | + if (element.events && element.events[type]) { |
874 | + delete element.events[type][handler.$$guid]; |
875 | + } |
876 | + } |
877 | + }; |
878 | + |
879 | + function handleEvent(event) { |
880 | + var returnValue = true; |
881 | + // grab the event object (IE uses a global event object) |
882 | + event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); |
883 | + // get a reference to the hash table of event handlers |
884 | + var handlers = this.events[event.type]; |
885 | + // execute each event handler |
886 | + for (var i in handlers) { |
887 | + this.$$handleEvent = handlers[i]; |
888 | + if (this.$$handleEvent(event) === false) { |
889 | + returnValue = false; |
890 | + } |
891 | + } |
892 | + return returnValue; |
893 | + }; |
894 | + |
895 | + function fixEvent(event) { |
896 | + // add W3C standard event methods |
897 | + event.preventDefault = fixEvent.preventDefault; |
898 | + event.stopPropagation = fixEvent.stopPropagation; |
899 | + return event; |
900 | + }; |
901 | + fixEvent.preventDefault = function() { |
902 | + this.returnValue = false; |
903 | + }; |
904 | + fixEvent.stopPropagation = function() { |
905 | + this.cancelBubble = true; |
906 | + } |
907 | + |
908 | + // Dean's forEach: http://dean.edwards.name/base/forEach.js |
909 | + /* |
910 | + forEach, version 1.0 |
911 | + Copyright 2006, Dean Edwards |
912 | + License: http://www.opensource.org/licenses/mit-license.php |
913 | + */ |
914 | + |
915 | + // array-like enumeration |
916 | + if (!Array.forEach) { // mozilla already supports this |
917 | + Array.forEach = function(array, block, context) { |
918 | + for (var i = 0; i < array.length; i++) { |
919 | + block.call(context, array[i], i, array); |
920 | + } |
921 | + }; |
922 | + } |
923 | + |
924 | + // generic enumeration |
925 | + Function.prototype.forEach = function(object, block, context) { |
926 | + for (var key in object) { |
927 | + if (typeof this.prototype[key] == "undefined") { |
928 | + block.call(context, object[key], key, object); |
929 | + } |
930 | + } |
931 | + }; |
932 | + |
933 | + // character enumeration |
934 | + String.forEach = function(string, block, context) { |
935 | + Array.forEach(string.split(""), function(chr, index) { |
936 | + block.call(context, chr, index, string); |
937 | + }); |
938 | + }; |
939 | + |
940 | + // globally resolve forEach enumeration |
941 | + var forEach = function(object, block, context) { |
942 | + if (object) { |
943 | + var resolve = Object; // default |
944 | + if (object instanceof Function) { |
945 | + // functions have a "length" property |
946 | + resolve = Function; |
947 | + } else if (object.forEach instanceof Function) { |
948 | + // the object implements a custom forEach method so use that |
949 | + object.forEach(block, context); |
950 | + return; |
951 | + } else if (typeof object == "string") { |
952 | + // the object is a string |
953 | + resolve = String; |
954 | + } else if (typeof object.length == "number") { |
955 | + // the object is array-like |
956 | + resolve = Array; |
957 | + } |
958 | + resolve.forEach(object, block, context); |
959 | + } |
960 | + }; |
961 | + |
962 | + namespace.SortTable = sorttable; |
963 | + |
964 | +}, "0.1", {}); |
965 | |
966 | === modified file 'lib/lp/app/javascript/tests/test_foldables.js' |
967 | --- lib/lp/app/javascript/tests/test_foldables.js 2012-01-10 17:36:29 +0000 |
968 | +++ lib/lp/app/javascript/tests/test_foldables.js 2012-01-13 14:12:26 +0000 |
969 | @@ -82,7 +82,6 @@ |
970 | }, |
971 | |
972 | test_doesnt_hide_short: function () { |
973 | - debugger; |
974 | this._add_comment(quote_comment); |
975 | Y.lp.app.foldables.activate(); |
976 | Y.Assert.isNull(Y.one('a')); |
977 | |
978 | === modified file 'lib/lp/app/templates/base-layout-macros.pt' |
979 | --- lib/lp/app/templates/base-layout-macros.pt 2012-01-11 13:59:39 +0000 |
980 | +++ lib/lp/app/templates/base-layout-macros.pt 2012-01-13 14:12:26 +0000 |
981 | @@ -106,10 +106,11 @@ |
982 | </script> |
983 | |
984 | <script id="base-layout-load-scripts" type="text/javascript"> |
985 | - LPS.use('node', 'event-delegate', 'lp', 'lp.app.foldables', 'lp.app.links', |
986 | - 'lp.app.longpoll', 'lp.app.inlinehelp', function(Y) { |
987 | + LPS.use('node', 'event-delegate', 'lp', 'lp.app.foldables', |
988 | + 'lp.app.links', 'lp.app.sorttable', 'lp.app.longpoll', |
989 | + 'lp.app.inlinehelp', function(Y) { |
990 | Y.on('load', function(e) { |
991 | - sortables_init(); |
992 | + Y.lp.app.sorttable.SortTable.init(); |
993 | Y.lp.app.inlinehelp.init_help(); |
994 | Y.lp.activate_collapsibles(); |
995 | Y.lp.app.foldables.activate(); |
996 | |
997 | === modified file 'lib/lp/archiveuploader/tests/nascentuploadfile.txt' |
998 | --- lib/lp/archiveuploader/tests/nascentuploadfile.txt 2011-12-30 06:14:56 +0000 |
999 | +++ lib/lp/archiveuploader/tests/nascentuploadfile.txt 2012-01-13 14:12:26 +0000 |
1000 | @@ -270,37 +270,6 @@ |
1001 | >>> addr['person'].creation_comment |
1002 | u'when the some-source_6.6.6 package was uploaded to hoary/RELEASE' |
1003 | |
1004 | -If the email address is registered but not associated with a person it will be |
1005 | -associated with a new Person. This involves updating the email address, |
1006 | -something for which the uploader must have explicit permissions (bug 589073). |
1007 | - |
1008 | - >>> sig_file.policy.create_people |
1009 | - True |
1010 | - |
1011 | - >>> from lp.services.database.sqlbase import commit |
1012 | - >>> from lp.services.identity.interfaces.account import IAccountSet |
1013 | - >>> from lp.registry.interfaces.person import ( |
1014 | - ... PersonCreationRationale, IPersonSet) |
1015 | - >>> (acct, email) = getUtility(IAccountSet).createAccountAndEmail( |
1016 | - ... "foo@canonical.com", PersonCreationRationale.UNKNOWN, |
1017 | - ... "fo", "secr1t") |
1018 | - >>> person = getUtility(IPersonSet).createPersonWithoutEmail("fo", |
1019 | - ... rationale=PersonCreationRationale.UNKNOWN) |
1020 | - >>> person.account = acct |
1021 | - |
1022 | - Commit the changes so the emailaddress will have to be updated later |
1023 | - rather than inserted. |
1024 | - |
1025 | - >>> commit() |
1026 | - |
1027 | - >>> addr = sig_file.parseAddress("Foo <foo@canonical.com>") |
1028 | - >>> print addr['person'].creation_rationale.name |
1029 | - UNKNOWN |
1030 | - >>> commit() |
1031 | - |
1032 | - >>> print addr['email'] |
1033 | - foo@canonical.com |
1034 | - |
1035 | If the use an un-initialized policy to create a launchpad person the |
1036 | creation_rationale will still be possible, however missing important |
1037 | information, the upload target: |
1038 | |
1039 | === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py' |
1040 | --- lib/lp/bugs/browser/tests/test_bugtask.py 2011-12-30 06:14:56 +0000 |
1041 | +++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-01-13 14:12:26 +0000 |
1042 | @@ -183,7 +183,7 @@ |
1043 | self.invalidate_caches(bug.default_bugtask) |
1044 | self.getUserBrowser(url, owner) |
1045 | # At least 20 of these should be removed. |
1046 | - self.assertThat(recorder, HasQueryCount(LessThan(106))) |
1047 | + self.assertThat(recorder, HasQueryCount(LessThan(107))) |
1048 | count_with_no_branches = recorder.count |
1049 | for sp in sourcepackages: |
1050 | self.makeLinkedBranchMergeProposal(sp, bug, owner) |
1051 | |
1052 | === modified file 'lib/lp/bugs/doc/externalbugtracker-comment-imports.txt' |
1053 | --- lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2011-12-29 05:29:36 +0000 |
1054 | +++ lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2012-01-13 14:12:26 +0000 |
1055 | @@ -179,23 +179,6 @@ |
1056 | >>> bug.messages[-1].owner.name |
1057 | u'no-priv' |
1058 | |
1059 | -This also works if the address is associated with an Account, but not a |
1060 | -Person. This should only happen when the user has logged into ShipIt but |
1061 | -not Launchpad. A new Person is created. |
1062 | - |
1063 | - >>> account = factory.makeAccount(email="account-only@example.com") |
1064 | - >>> external_bugtracker.poster_tuple = ( |
1065 | - ... 'Account Only', 'account-only@example.com') |
1066 | - >>> external_bugtracker.remote_comments['account-only-comment'] = ( |
1067 | - ... "Account-only comment.") |
1068 | - >>> bugwatch_updater.importBugComments() |
1069 | - INFO:...:Imported 1 comments for remote bug 123456 on ... |
1070 | - |
1071 | - >>> bug.messages[-1].owner.name |
1072 | - u'account-only' |
1073 | - >>> bug.messages[-1].owner.account == account |
1074 | - True |
1075 | - |
1076 | It's also possible for Launchpad to create Persons from remote |
1077 | bugtracker users when the remote bugtracker doesn't specify an email |
1078 | address. In those cases, the ExternalBugTracker's getPosterForComment() |
1079 | |
1080 | === modified file 'lib/lp/bugs/scripts/bugimport.py' |
1081 | --- lib/lp/bugs/scripts/bugimport.py 2011-12-30 06:14:56 +0000 |
1082 | +++ lib/lp/bugs/scripts/bugimport.py 2012-01-13 14:12:26 +0000 |
1083 | @@ -168,8 +168,9 @@ |
1084 | person = None |
1085 | |
1086 | if person is None: |
1087 | - address = getUtility(IEmailAddressSet).getByEmail(email) |
1088 | - if address is None: |
1089 | + person = getUtility(IPersonSet).getByEmail(email) |
1090 | + |
1091 | + if person is None: |
1092 | self.logger.debug('creating person for %s' % email) |
1093 | # Has the short name been taken? |
1094 | if name is not None and ( |
1095 | @@ -184,26 +185,6 @@ |
1096 | rationale=PersonCreationRationale.BUGIMPORT, |
1097 | comment=('when importing bugs for %s' % |
1098 | self.product.displayname))) |
1099 | - elif address.personID is None: |
1100 | - # The user has an Account and and EmailAddress linked |
1101 | - # to that account. |
1102 | - assert address.accountID is not None, ( |
1103 | - "Email address not linked to an Account: %s " % email) |
1104 | - self.logger.debug( |
1105 | - 'creating person from account for %s' % email) |
1106 | - if name is not None and ( |
1107 | - person_set.getByName(name) is not None): |
1108 | - # The short name is already taken, so we'll pass |
1109 | - # None to createPerson(), which will take care of |
1110 | - # creating a unique one. |
1111 | - name = None |
1112 | - person = address.account.createPerson( |
1113 | - rationale=PersonCreationRationale.BUGIMPORT, |
1114 | - name=name, comment=('when importing bugs for %s' % |
1115 | - self.product.displayname)) |
1116 | - else: |
1117 | - # EmailAddress and Person are in different stores. |
1118 | - person = person_set.get(address.personID) |
1119 | |
1120 | self.person_id_cache[email] = person.id |
1121 | |
1122 | |
1123 | === modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py' |
1124 | --- lib/lp/bugs/scripts/tests/test_bugimport.py 2012-01-05 00:15:32 +0000 |
1125 | +++ lib/lp/bugs/scripts/tests/test_bugimport.py 2012-01-13 14:12:26 +0000 |
1126 | @@ -297,34 +297,6 @@ |
1127 | self.assertNotEqual(person.preferredemail, None) |
1128 | self.assertEqual(person.preferredemail.email, 'foo@preferred.com') |
1129 | |
1130 | - def test_person_from_account(self): |
1131 | - # If an Account record exists for a user's email address, but |
1132 | - # no Person record is linked to it, the bug importer creates a |
1133 | - # Person and links the three piece of information together. |
1134 | - account = self.factory.makeAccount("Sam") |
1135 | - personnode = ET.fromstring( |
1136 | - '<person xmlns="https://launchpad.net/xmlns/2006/bugs" />') |
1137 | - personnode.set('name', generate_nick(account.preferredemail.email)) |
1138 | - personnode.set('email', account.preferredemail.email) |
1139 | - personnode.text = account.displayname |
1140 | - |
1141 | - product = getUtility(IProductSet).getByName('netapplet') |
1142 | - importer = bugimport.BugImporter( |
1143 | - product, 'bugs.xml', 'bug-map.pickle', verify_users=True) |
1144 | - person = importer.getPerson(personnode) |
1145 | - |
1146 | - # The person returned is associated with the account. |
1147 | - self.failUnlessEqual(account.id, person.accountID) |
1148 | - # The creation comment and rationale are set correctly. |
1149 | - self.failUnlessEqual( |
1150 | - 'when importing bugs for %s' % product.displayname, |
1151 | - person.creation_comment) |
1152 | - self.failUnlessEqual( |
1153 | - PersonCreationRationale.BUGIMPORT, |
1154 | - person.creation_rationale) |
1155 | - # The person's email addresses are hidden by default. |
1156 | - self.failUnless(person.hide_email_addresses) |
1157 | - |
1158 | |
1159 | class GetMilestoneTestCase(unittest.TestCase): |
1160 | """Tests for the BugImporter.getMilestone() method.""" |
1161 | |
1162 | === modified file 'lib/lp/bugs/templates/buglisting.mustache' |
1163 | --- lib/lp/bugs/templates/buglisting.mustache 2011-12-20 00:00:03 +0000 |
1164 | +++ lib/lp/bugs/templates/buglisting.mustache 2012-01-13 14:12:26 +0000 |
1165 | @@ -22,60 +22,60 @@ |
1166 | {{/show_id}} |
1167 | <a href="{{bug_url}}" class="bugtitle">{{title}}</a> |
1168 | </div> |
1169 | - </div> |
1170 | - <div class="buglisting-col3"> |
1171 | - {{#show_targetname}} |
1172 | - <span class="{{bugtarget_css}} field"> |
1173 | - {{bugtarget}} |
1174 | - </span> |
1175 | - {{/show_targetname}} |
1176 | - {{#show_milestone_name}} |
1177 | - <span class="sprite milestone field"> |
1178 | - {{#milestone_name}} |
1179 | - {{milestone_name}} |
1180 | - {{/milestone_name}} |
1181 | - {{^milestone_name}} |
1182 | - No milestone set |
1183 | - {{/milestone_name}} |
1184 | - </span> |
1185 | - {{/show_milestone_name}} |
1186 | - {{#show_date_last_updated}} |
1187 | - <span class="sprite milestone field"> |
1188 | - Last updated {{last_updated}} |
1189 | - </span> |
1190 | - {{/show_date_last_updated}} |
1191 | - {{#show_assignee}} |
1192 | - <span class="sprite person field"> |
1193 | - {{#assignee}}Assignee: {{assignee}}{{/assignee}} |
1194 | - {{^assignee}}Assignee: None{{/assignee}} |
1195 | - </span> |
1196 | - {{/show_assignee}} |
1197 | - {{#show_reporter}} |
1198 | - <span class="sprite person field"> |
1199 | - Reporter: {{reporter}} |
1200 | - </span> |
1201 | - {{/show_reporter}} |
1202 | - {{#show_datecreated}} |
1203 | - <span class="sprite milestone field"> |
1204 | - {{age}} |
1205 | - </span> |
1206 | - {{/show_datecreated}} |
1207 | - {{#show_tag}} |
1208 | - <span class="field">Tags: |
1209 | - {{^has_tags}}None{{/has_tags}} |
1210 | - {{#tags}} |
1211 | - <a href="{{url}}" class="tag">{{tag}}</a>  |
1212 | - {{/tags}} |
1213 | - </span> |
1214 | - {{/show_tag}} |
1215 | - {{#show_heat}} |
1216 | - <span class="bug-heat-icons"> |
1217 | - {{{bug_heat_html}}} |
1218 | - </span> |
1219 | - {{/show_heat}} |
1220 | - <span class="bug-related-icons"> |
1221 | - {{{badges}}} |
1222 | - </span> |
1223 | + <div class="buginfo-extra"> |
1224 | + {{#show_targetname}} |
1225 | + <span class="{{bugtarget_css}} field"> |
1226 | + {{bugtarget}} |
1227 | + </span> |
1228 | + {{/show_targetname}} |
1229 | + {{#show_milestone_name}} |
1230 | + <span class="sprite milestone field"> |
1231 | + {{#milestone_name}} |
1232 | + {{milestone_name}} |
1233 | + {{/milestone_name}} |
1234 | + {{^milestone_name}} |
1235 | + No milestone set |
1236 | + {{/milestone_name}} |
1237 | + </span> |
1238 | + {{/show_milestone_name}} |
1239 | + {{#show_date_last_updated}} |
1240 | + <span class="sprite milestone field"> |
1241 | + Last updated {{last_updated}} |
1242 | + </span> |
1243 | + {{/show_date_last_updated}} |
1244 | + {{#show_assignee}} |
1245 | + <span class="sprite person field"> |
1246 | + {{#assignee}}Assignee: {{assignee}}{{/assignee}} |
1247 | + {{^assignee}}Assignee: None{{/assignee}} |
1248 | + </span> |
1249 | + {{/show_assignee}} |
1250 | + {{#show_reporter}} |
1251 | + <span class="sprite person field"> |
1252 | + Reporter: {{reporter}} |
1253 | + </span> |
1254 | + {{/show_reporter}} |
1255 | + {{#show_datecreated}} |
1256 | + <span class="sprite milestone field"> |
1257 | + {{age}} |
1258 | + </span> |
1259 | + {{/show_datecreated}} |
1260 | + {{#show_tag}} |
1261 | + <span class="field">Tags: |
1262 | + {{^has_tags}}None{{/has_tags}} |
1263 | + {{#tags}} |
1264 | + <a href="{{url}}" class="tag">{{tag}}</a>  |
1265 | + {{/tags}} |
1266 | + </span> |
1267 | + {{/show_tag}} |
1268 | + {{#show_heat}} |
1269 | + <span class="bug-heat-icons"> |
1270 | + {{{bug_heat_html}}} |
1271 | + </span> |
1272 | + {{/show_heat}} |
1273 | + <span class="bug-related-icons"> |
1274 | + {{{badges}}} |
1275 | + </span> |
1276 | + </div> |
1277 | </div> |
1278 | |
1279 | </div> |
1280 | |
1281 | === modified file 'lib/lp/registry/doc/person-account.txt' |
1282 | --- lib/lp/registry/doc/person-account.txt 2011-12-18 13:45:20 +0000 |
1283 | +++ lib/lp/registry/doc/person-account.txt 2012-01-13 14:12:26 +0000 |
1284 | @@ -31,8 +31,7 @@ |
1285 | the profile. Sample Person cannot claim it. |
1286 | |
1287 | >>> login('test@canonical.com') |
1288 | - >>> matsubara.account.activate( |
1289 | - ... comment="test", password='ok', preferred_email=emailaddress) |
1290 | + >>> matsubara.account.reactivate(comment="test", password='ok') |
1291 | Traceback (most recent call last): |
1292 | ... |
1293 | Unauthorized: ...'launchpad.Special') |
1294 | @@ -42,8 +41,8 @@ |
1295 | |
1296 | >>> from zope.security.proxy import removeSecurityProxy |
1297 | >>> login('matsubara@async.com.br') |
1298 | - >>> matsubara.account.activate( |
1299 | - ... comment="test", password='ok', preferred_email=emailaddress) |
1300 | + >>> matsubara.account.reactivate(comment="test", password='ok') |
1301 | + >>> matsubara.setPreferredEmail(emailaddress) |
1302 | >>> import transaction |
1303 | >>> transaction.commit() |
1304 | >>> matsubara.is_valid_person |
1305 | @@ -52,7 +51,7 @@ |
1306 | <DBItem AccountStatus.ACTIVE, ...> |
1307 | >>> matsubara.account.status_comment |
1308 | u'test' |
1309 | - >>> removeSecurityProxy(matsubara.account.preferredemail).email |
1310 | + >>> removeSecurityProxy(matsubara.preferredemail).email |
1311 | u'matsubara@async.com.br' |
1312 | |
1313 | |
1314 | @@ -212,15 +211,7 @@ |
1315 | Reactivating user accounts |
1316 | -------------------------- |
1317 | |
1318 | -Accounts can be reactivated. A comment and a non-None preferred email address |
1319 | -are required to reactivate() an account, though. |
1320 | - |
1321 | - >>> foobar.account.reactivate( |
1322 | - ... 'This will raise an error.', password='', preferred_email=None) |
1323 | - Traceback (most recent call last): |
1324 | - ... |
1325 | - AssertionError: Account ... cannot be activated without a preferred |
1326 | - email address. |
1327 | +Accounts can be reactivated. |
1328 | |
1329 | >>> foobar.reactivate( |
1330 | ... 'User reactivated the account using reset password.', |
1331 | |
1332 | === modified file 'lib/lp/registry/doc/person.txt' |
1333 | --- lib/lp/registry/doc/person.txt 2012-01-04 23:49:46 +0000 |
1334 | +++ lib/lp/registry/doc/person.txt 2012-01-13 14:12:26 +0000 |
1335 | @@ -132,9 +132,6 @@ |
1336 | >>> p.account_status |
1337 | <DBItem AccountStatus.NOACCOUNT... |
1338 | |
1339 | - >>> email.accountID == p.accountID |
1340 | - True |
1341 | - |
1342 | >>> p.setPreferredEmail(email) |
1343 | >>> email.status |
1344 | <DBItem EmailAddressStatus.PREFERRED... |
1345 | @@ -145,10 +142,7 @@ |
1346 | >>> from lp.services.identity.model.account import Account |
1347 | >>> from lp.services.database.lpstorm import IMasterStore |
1348 | >>> account = IMasterStore(Account).get(Account, p.accountID) |
1349 | - >>> account.activate( |
1350 | - ... "Activated by doc test.", |
1351 | - ... password=p.password, |
1352 | - ... preferred_email=email) |
1353 | + >>> account.reactivate("Activated by doc test.", password=p.password) |
1354 | >>> p.account_status |
1355 | <DBItem AccountStatus.ACTIVE... |
1356 | |
1357 | |
1358 | === modified file 'lib/lp/registry/model/person.py' |
1359 | --- lib/lp/registry/model/person.py 2012-01-06 19:56:39 +0000 |
1360 | +++ lib/lp/registry/model/person.py 2012-01-13 14:12:26 +0000 |
1361 | @@ -2540,7 +2540,8 @@ |
1362 | def reactivate(self, comment, password, preferred_email): |
1363 | """See `IPersonSpecialRestricted`.""" |
1364 | account = IMasterObject(self.account) |
1365 | - account.reactivate(comment, password, preferred_email) |
1366 | + account.reactivate(comment, password) |
1367 | + self.setPreferredEmail(preferred_email) |
1368 | if '-deactivatedaccount' in self.name: |
1369 | # The name was changed by deactivateAccount(). Restore the |
1370 | # name, but we must ensure it does not conflict with a current |
1371 | @@ -3108,97 +3109,81 @@ |
1372 | # possible replication lag issues but this might actually be |
1373 | # unnecessary. |
1374 | with MasterDatabasePolicy(): |
1375 | - store = IMasterStore(EmailAddress) |
1376 | - join = store.using( |
1377 | - EmailAddress, |
1378 | - LeftJoin(Account, EmailAddress.accountID == Account.id)) |
1379 | - email, account = ( |
1380 | - join.find( |
1381 | - (EmailAddress, Account), |
1382 | - EmailAddress.email.lower() == |
1383 | - ensure_unicode(email_address).lower()).one() |
1384 | + email, person = ( |
1385 | + getUtility(IPersonSet).getByEmails([email_address]).one() |
1386 | or (None, None)) |
1387 | - identifier = store.find( |
1388 | + identifier = IStore(OpenIdIdentifier).find( |
1389 | OpenIdIdentifier, identifier=openid_identifier).one() |
1390 | |
1391 | - if email is None and identifier is None: |
1392 | - # Neither the Email Address not the OpenId Identifier |
1393 | - # exist in the database. Create the email address, |
1394 | - # account, and associated info. OpenIdIdentifier is |
1395 | - # created later. |
1396 | - account_set = getUtility(IAccountSet) |
1397 | - account, email = account_set.createAccountAndEmail( |
1398 | - email_address, creation_rationale, full_name, |
1399 | - password=None) |
1400 | - db_updated = True |
1401 | - |
1402 | - elif email is None: |
1403 | - # The Email Address does not exist in the database, |
1404 | - # but the OpenId Identifier does. Create the Email |
1405 | - # Address and link it to the account. |
1406 | - assert account is None, 'Retrieved an account but not email?' |
1407 | - account = identifier.account |
1408 | - emailaddress_set = getUtility(IEmailAddressSet) |
1409 | - email = emailaddress_set.new( |
1410 | - email_address, account=account) |
1411 | - db_updated = True |
1412 | - |
1413 | - elif account is None: |
1414 | - # Email address exists, but there is no Account linked |
1415 | - # to it. Create the Account and link it to the |
1416 | - # EmailAddress. |
1417 | + if email is None: |
1418 | + if identifier is None: |
1419 | + # Neither the Email Address not the OpenId Identifier |
1420 | + # exist in the database. Create the email address, |
1421 | + # account, and associated info. OpenIdIdentifier is |
1422 | + # created later. |
1423 | + person_set = getUtility(IPersonSet) |
1424 | + person, email = person_set.createPersonAndEmail( |
1425 | + email_address, creation_rationale, comment=comment, |
1426 | + displayname=full_name) |
1427 | + db_updated = True |
1428 | + else: |
1429 | + # The Email Address does not exist in the database, |
1430 | + # but the OpenId Identifier does. Create the Email |
1431 | + # Address and link it to the person. |
1432 | + person = IPerson(identifier.account, None) |
1433 | + assert person is not None, ( |
1434 | + 'Received a personless account.') |
1435 | + emailaddress_set = getUtility(IEmailAddressSet) |
1436 | + email = emailaddress_set.new(email_address, person=person) |
1437 | + db_updated = True |
1438 | + elif email.person.account is None: |
1439 | + # Email address and person exist, but there is no |
1440 | + # account. Create and link it. |
1441 | account_set = getUtility(IAccountSet) |
1442 | account = account_set.new( |
1443 | AccountCreationRationale.OWNER_CREATED_LAUNCHPAD, |
1444 | full_name) |
1445 | - email.account = account |
1446 | + removeSecurityProxy(email.person).account = account |
1447 | db_updated = True |
1448 | |
1449 | + person = email.person |
1450 | + assert person.account is not None |
1451 | + |
1452 | if identifier is None: |
1453 | # This is the first time we have seen that |
1454 | # OpenIdIdentifier. Link it. |
1455 | identifier = OpenIdIdentifier() |
1456 | - identifier.account = account |
1457 | + identifier.account = person.account |
1458 | identifier.identifier = openid_identifier |
1459 | - store.add(identifier) |
1460 | + IStore(OpenIdIdentifier).add(identifier) |
1461 | db_updated = True |
1462 | - |
1463 | - elif identifier.account != account: |
1464 | + elif identifier.account != person.account: |
1465 | # The ISD OpenId server may have linked this OpenId |
1466 | # identifier to a new email address, or the user may |
1467 | # have transfered their email address to a different |
1468 | # Launchpad Account. If that happened, repair the |
1469 | # link - we trust the ISD OpenId server. |
1470 | - identifier.account = account |
1471 | + identifier.account = person.account |
1472 | db_updated = True |
1473 | |
1474 | # We now have an account, email address, and openid identifier. |
1475 | |
1476 | - if account.status == AccountStatus.SUSPENDED: |
1477 | + if person.account.status == AccountStatus.SUSPENDED: |
1478 | raise AccountSuspendedError( |
1479 | "The account matching the identifier is suspended.") |
1480 | |
1481 | - elif account.status in [AccountStatus.DEACTIVATED, |
1482 | - AccountStatus.NOACCOUNT]: |
1483 | - password = '' # Needed just to please reactivate() below. |
1484 | - removeSecurityProxy(account).reactivate( |
1485 | - comment, password, removeSecurityProxy(email)) |
1486 | + elif person.account.status in [AccountStatus.DEACTIVATED, |
1487 | + AccountStatus.NOACCOUNT]: |
1488 | + password = '' |
1489 | + removeSecurityProxy(person.account).reactivate( |
1490 | + comment, password) |
1491 | + removeSecurityProxy(person).setPreferredEmail(email) |
1492 | db_updated = True |
1493 | else: |
1494 | # Account is active, so nothing to do. |
1495 | pass |
1496 | |
1497 | - if IPerson(account, None) is None: |
1498 | - removeSecurityProxy(account).createPerson( |
1499 | - creation_rationale, comment=comment) |
1500 | - db_updated = True |
1501 | - |
1502 | - person = IPerson(account) |
1503 | - if email.personID != person.id: |
1504 | - removeSecurityProxy(email).person = person |
1505 | - db_updated = True |
1506 | - |
1507 | - return person, db_updated |
1508 | + return email.person, db_updated |
1509 | |
1510 | def newTeam(self, teamowner, name, displayname, teamdescription=None, |
1511 | subscriptionpolicy=TeamSubscriptionPolicy.MODERATED, |
1512 | @@ -3251,12 +3236,8 @@ |
1513 | person = self._newPerson( |
1514 | name, displayname, hide_email_addresses, rationale=rationale, |
1515 | comment=comment, registrant=registrant, account=account) |
1516 | - |
1517 | - email = getUtility(IEmailAddressSet).new( |
1518 | - email, person, account=account) |
1519 | - |
1520 | - assert email.accountID is not None, ( |
1521 | - 'Failed to link EmailAddress to Account') |
1522 | + email = getUtility(IEmailAddressSet).new(email, person) |
1523 | + |
1524 | return person, email |
1525 | |
1526 | def createPersonWithoutEmail( |
1527 | @@ -3299,55 +3280,14 @@ |
1528 | def ensurePerson(self, email, displayname, rationale, comment=None, |
1529 | registrant=None): |
1530 | """See `IPersonSet`.""" |
1531 | - # Start by checking if there is an `EmailAddress` for the given |
1532 | - # text address. There are many cases where an email address can be |
1533 | - # created without an associated `Person`. For instance, we created |
1534 | - # an account linked to the address through an external system such |
1535 | - # SSO or ShipIt. |
1536 | - email_address = getUtility(IEmailAddressSet).getByEmail(email) |
1537 | + person = getUtility(IPersonSet).getByEmail(email) |
1538 | |
1539 | - # There is no `EmailAddress` for this text address, so we need to |
1540 | - # create both the `Person` and `EmailAddress` here and we are done. |
1541 | - if email_address is None: |
1542 | + if person is None: |
1543 | person, email_address = self.createPersonAndEmail( |
1544 | email, rationale, comment=comment, displayname=displayname, |
1545 | registrant=registrant, hide_email_addresses=True) |
1546 | - return person |
1547 | - |
1548 | - # There is an `EmailAddress` for this text address, but no |
1549 | - # associated `Person`. |
1550 | - if email_address.person is None: |
1551 | - assert email_address.accountID is not None, ( |
1552 | - '%s is not associated to a person or account' |
1553 | - % email_address.email) |
1554 | - account = IMasterStore(Account).get( |
1555 | - Account, email_address.accountID) |
1556 | - account_person = self.getByAccount(account) |
1557 | - if account_person is None: |
1558 | - # There is no associated `Person` to the email `Account`. |
1559 | - # This is probably because the account was created externally |
1560 | - # to Launchpad. Create just the `Person`, associate it with |
1561 | - # the `EmailAddress` and return it. |
1562 | - name = generate_nick(email) |
1563 | - account_person = self._newPerson( |
1564 | - name, displayname, hide_email_addresses=True, |
1565 | - rationale=rationale, comment=comment, |
1566 | - registrant=registrant, account=email_address.account) |
1567 | - # There is (now) a `Person` linked to the `Account`, link the |
1568 | - # `EmailAddress` to this `Person` and return it. |
1569 | - master_email = IMasterStore(EmailAddress).get( |
1570 | - EmailAddress, email_address.id) |
1571 | - master_email.personID = account_person.id |
1572 | - # Populate the previously empty 'preferredemail' cached |
1573 | - # property, so the Person record is up-to-date. |
1574 | - if master_email.status == EmailAddressStatus.PREFERRED: |
1575 | - cache = get_property_cache(account_person) |
1576 | - cache.preferredemail = master_email |
1577 | - return account_person |
1578 | - |
1579 | - # Easy, return the `Person` associated with the existing |
1580 | - # `EmailAddress`. |
1581 | - return IMasterStore(Person).get(Person, email_address.personID) |
1582 | + |
1583 | + return person |
1584 | |
1585 | def getByName(self, name, ignore_merged=True): |
1586 | """See `IPersonSet`.""" |
1587 | |
1588 | === modified file 'lib/lp/registry/tests/test_mailinglistapi.py' |
1589 | --- lib/lp/registry/tests/test_mailinglistapi.py 2012-01-01 02:58:52 +0000 |
1590 | +++ lib/lp/registry/tests/test_mailinglistapi.py 2012-01-13 14:12:26 +0000 |
1591 | @@ -73,10 +73,6 @@ |
1592 | def test_isRegisteredInLaunchpad_email_no_email_address(self): |
1593 | self.assertFalse(self.api.isRegisteredInLaunchpad('me@fndor.dom')) |
1594 | |
1595 | - def test_isRegisteredInLaunchpad_email_without_person(self): |
1596 | - self.factory.makeAccount('Me', email='me@fndor.dom') |
1597 | - self.assertFalse(self.api.isRegisteredInLaunchpad('me@fndor.dom')) |
1598 | - |
1599 | def test_isRegisteredInLaunchpad_archive_address_is_false(self): |
1600 | # The Mailman archive address can never be owned by an Lp user |
1601 | # because such a user would have acces to all lists. |
1602 | |
1603 | === modified file 'lib/lp/registry/tests/test_personset.py' |
1604 | --- lib/lp/registry/tests/test_personset.py 2012-01-06 15:14:48 +0000 |
1605 | +++ lib/lp/registry/tests/test_personset.py 2012-01-13 14:12:26 +0000 |
1606 | @@ -229,7 +229,6 @@ |
1607 | email = from_person.preferredemail |
1608 | email.status = EmailAddressStatus.NEW |
1609 | email.person = to_person |
1610 | - email.account = to_person.account |
1611 | transaction.commit() |
1612 | |
1613 | def _do_merge(self, from_person, to_person, reviewer=None): |
1614 | @@ -725,12 +724,10 @@ |
1615 | |
1616 | # The old email address is still there and correctly linked. |
1617 | self.assertIs(self.email, found.preferredemail) |
1618 | - self.assertIs(self.email.account, self.account) |
1619 | self.assertIs(self.email.person, self.person) |
1620 | |
1621 | # The new email address is there too and correctly linked. |
1622 | new_email = self.store.find(EmailAddress, email=new_email).one() |
1623 | - self.assertIs(new_email.account, self.account) |
1624 | self.assertIs(new_email.person, self.person) |
1625 | self.assertEqual(EmailAddressStatus.NEW, new_email.status) |
1626 | |
1627 | @@ -751,32 +748,9 @@ |
1628 | # It is correctly linked to an account, emailaddress and |
1629 | # identifier. |
1630 | self.assertIs(found, found.preferredemail.person) |
1631 | - self.assertIs(found.account, found.preferredemail.account) |
1632 | self.assertEqual( |
1633 | new_identifier, found.account.openid_identifiers.any().identifier) |
1634 | |
1635 | - def testNoPerson(self): |
1636 | - # If the account is not linked to a Person, create one. ShipIt |
1637 | - # users fall into this category the first time they log into |
1638 | - # Launchpad. |
1639 | - self.email.person = None |
1640 | - self.person.account = None |
1641 | - |
1642 | - found, updated = self.person_set.getOrCreateByOpenIDIdentifier( |
1643 | - self.identifier.identifier, self.email.email, 'New Name', |
1644 | - PersonCreationRationale.UNKNOWN, 'No Comment') |
1645 | - found = removeSecurityProxy(found) |
1646 | - |
1647 | - # We have a new Person |
1648 | - self.assertIs(True, updated) |
1649 | - self.assertIsNot(self.person, found) |
1650 | - |
1651 | - # It is correctly linked to an account, emailaddress and |
1652 | - # identifier. |
1653 | - self.assertIs(found, found.preferredemail.person) |
1654 | - self.assertIs(found.account, found.preferredemail.account) |
1655 | - self.assertIn(self.identifier, list(found.account.openid_identifiers)) |
1656 | - |
1657 | def testNoAccount(self): |
1658 | # EmailAddress is linked to a Person, but there is no Account. |
1659 | # Convert this stub into something valid. |
1660 | @@ -795,7 +769,6 @@ |
1661 | self.assertEqual( |
1662 | new_identifier, found.account.openid_identifiers.any().identifier) |
1663 | self.assertIs(self.email.person, found) |
1664 | - self.assertIs(self.email.account, found.account) |
1665 | self.assertEqual(EmailAddressStatus.PREFERRED, self.email.status) |
1666 | |
1667 | def testMovedEmailAddress(self): |
1668 | @@ -916,41 +889,6 @@ |
1669 | self.email_address, self.displayname, self.rationale) |
1670 | self.assertTrue(ensured_person.hide_email_addresses) |
1671 | |
1672 | - def test_ensurePerson_for_existing_account(self): |
1673 | - # IPerson.ensurePerson creates missing Person for existing |
1674 | - # Accounts. |
1675 | - test_account = self.factory.makeAccount( |
1676 | - self.displayname, email=self.email_address) |
1677 | - self.assertIs(None, test_account.preferredemail.person) |
1678 | - |
1679 | - ensured_person = self.person_set.ensurePerson( |
1680 | - self.email_address, self.displayname, self.rationale) |
1681 | - self.assertEquals(test_account.id, ensured_person.account.id) |
1682 | - self.assertEquals( |
1683 | - test_account.preferredemail, ensured_person.preferredemail) |
1684 | - self.assertEquals(ensured_person, test_account.preferredemail.person) |
1685 | - self.assertTrue(ensured_person.hide_email_addresses) |
1686 | - |
1687 | - def test_ensurePerson_for_existing_account_with_person(self): |
1688 | - # IPerson.ensurePerson return existing Person for existing |
1689 | - # Accounts and additionally bounds the account email to the |
1690 | - # Person in question. |
1691 | - |
1692 | - # Create a testing `Account` and a testing `Person` directly, |
1693 | - # linked. |
1694 | - testing_account = self.factory.makeAccount( |
1695 | - self.displayname, email=self.email_address) |
1696 | - testing_person = removeSecurityProxy( |
1697 | - testing_account).createPerson(self.rationale) |
1698 | - self.assertEqual( |
1699 | - testing_person, testing_account.preferredemail.person) |
1700 | - |
1701 | - # Since there's an existing Person for the given email address, |
1702 | - # IPersonSet.ensurePerson() will just return it. |
1703 | - ensured_person = self.person_set.ensurePerson( |
1704 | - self.email_address, self.displayname, self.rationale) |
1705 | - self.assertEqual(testing_person, ensured_person) |
1706 | - |
1707 | |
1708 | class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory): |
1709 | |
1710 | @@ -977,25 +915,6 @@ |
1711 | self.assertEqual(person, result) |
1712 | self.assertFalse(db_updated) |
1713 | |
1714 | - def test_existing_account_no_person(self): |
1715 | - # A person is created with the correct rationale. |
1716 | - account = self.factory.makeAccount('purchaser') |
1717 | - openid_ident = removeSecurityProxy( |
1718 | - account).openid_identifiers.any().identifier |
1719 | - |
1720 | - person, db_updated = self.callGetOrCreate(openid_ident) |
1721 | - |
1722 | - self.assertEqual(account, person.account) |
1723 | - # The person is created with the correct rationale and creation |
1724 | - # comment. |
1725 | - self.assertEqual( |
1726 | - "when purchasing an application via Software Center.", |
1727 | - person.creation_comment) |
1728 | - self.assertEqual( |
1729 | - PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, |
1730 | - person.creation_rationale) |
1731 | - self.assertTrue(db_updated) |
1732 | - |
1733 | def test_existing_deactivated_account(self): |
1734 | # An existing deactivated account will be reactivated. |
1735 | person = self.factory.makePerson( |
1736 | |
1737 | === modified file 'lib/lp/registry/xmlrpc/mailinglist.py' |
1738 | --- lib/lp/registry/xmlrpc/mailinglist.py 2012-01-01 02:58:52 +0000 |
1739 | +++ lib/lp/registry/xmlrpc/mailinglist.py 2012-01-13 14:12:26 +0000 |
1740 | @@ -225,7 +225,6 @@ |
1741 | return False |
1742 | email_address = getUtility(IEmailAddressSet).getByEmail(address) |
1743 | return (email_address is not None and |
1744 | - email_address.personID is not None and |
1745 | not email_address.person.is_team and |
1746 | email_address.status in (EmailAddressStatus.VALIDATED, |
1747 | EmailAddressStatus.PREFERRED)) |
1748 | |
1749 | === modified file 'lib/lp/scripts/garbo.py' |
1750 | --- lib/lp/scripts/garbo.py 2012-01-06 18:59:36 +0000 |
1751 | +++ lib/lp/scripts/garbo.py 2012-01-13 14:12:26 +0000 |
1752 | @@ -701,7 +701,7 @@ |
1753 | AND Person.id IN (%s) |
1754 | """ % people_ids) |
1755 | self.store.execute(""" |
1756 | - UPDATE EmailAddress SET person=NULL |
1757 | + DELETE FROM EmailAddress |
1758 | WHERE person IN (%s) |
1759 | """ % people_ids) |
1760 | # This cascade deletes any PersonSettings records. |
1761 | |
1762 | === modified file 'lib/lp/security.py' |
1763 | --- lib/lp/security.py 2012-01-06 17:35:41 +0000 |
1764 | +++ lib/lp/security.py 2012-01-13 14:12:26 +0000 |
1765 | @@ -147,7 +147,6 @@ |
1766 | from lp.registry.interfaces.role import ( |
1767 | IHasDrivers, |
1768 | IHasOwner, |
1769 | - IPersonRoles, |
1770 | ) |
1771 | from lp.registry.interfaces.sourcepackage import ISourcePackage |
1772 | from lp.registry.interfaces.teammembership import ( |
1773 | @@ -168,6 +167,7 @@ |
1774 | IOAuthAccessToken, |
1775 | IOAuthRequestToken, |
1776 | ) |
1777 | +from lp.services.webapp.authorization import check_permission |
1778 | from lp.services.openid.interfaces.openididentifier import IOpenIdIdentifier |
1779 | from lp.services.webapp.interfaces import ILaunchpadRoot |
1780 | from lp.services.worlddata.interfaces.country import ICountry |
1781 | @@ -237,6 +237,30 @@ |
1782 | return True |
1783 | |
1784 | |
1785 | +class LimitedViewDeferredToView(AuthorizationBase): |
1786 | + """The default ruleset for the launchpad.LimitedView permission. |
1787 | + |
1788 | + Few objects define LimitedView permission because it is only needed |
1789 | + in cases where a user may know something about a private object. The |
1790 | + default behaviour is to check if the user has launchpad.View permission; |
1791 | + private objects must define their own launchpad.LimitedView checker to |
1792 | + trully check the permission. |
1793 | + """ |
1794 | + permission = 'launchpad.LimitedView' |
1795 | + usedfor = Interface |
1796 | + |
1797 | + def checkUnauthenticated(self): |
1798 | + # The forward adapter approach is not reliable because the object |
1799 | + # might not define a permission checker for launchpad.View. |
1800 | + # eg. IHasMilestones is implicitly public to anonymous users, |
1801 | + # there is no nearest adapter to call checkUnauthenticated. |
1802 | + return check_permission('launchpad.View', self.obj) |
1803 | + |
1804 | + def checkAuthenticated(self, user): |
1805 | + return self.forwardCheckAuthenticated( |
1806 | + user, self.obj, 'launchpad.View') |
1807 | + |
1808 | + |
1809 | class AdminByAdminsTeam(AuthorizationBase): |
1810 | permission = 'launchpad.Admin' |
1811 | usedfor = Interface |
1812 | @@ -2632,7 +2656,7 @@ |
1813 | # Anonymous users can never see email addresses. |
1814 | return False |
1815 | |
1816 | - def checkAccountAuthenticated(self, account): |
1817 | + def checkAuthenticated(self, user): |
1818 | """Can the user see the details of this email address? |
1819 | |
1820 | If the email address' owner doesn't want his email addresses to be |
1821 | @@ -2640,17 +2664,13 @@ |
1822 | admins can see them. |
1823 | """ |
1824 | # Always allow users to see their own email addresses. |
1825 | - if self.obj.account == account: |
1826 | + if self.obj.person == user: |
1827 | return True |
1828 | |
1829 | if not (self.obj.person is None or |
1830 | self.obj.person.hide_email_addresses): |
1831 | return True |
1832 | |
1833 | - user = IPersonRoles(IPerson(account, None), None) |
1834 | - if user is None: |
1835 | - return False |
1836 | - |
1837 | return (self.obj.person is not None and user.inTeam(self.obj.person) |
1838 | or user.in_commercial_admin |
1839 | or user.in_registry_experts |
1840 | @@ -2661,12 +2681,11 @@ |
1841 | permission = 'launchpad.Edit' |
1842 | usedfor = IEmailAddress |
1843 | |
1844 | - def checkAccountAuthenticated(self, account): |
1845 | + def checkAuthenticated(self, user): |
1846 | # Always allow users to see their own email addresses. |
1847 | - if self.obj.account == account: |
1848 | + if self.obj.person == user: |
1849 | return True |
1850 | - return super(EditEmailAddress, self).checkAccountAuthenticated( |
1851 | - account) |
1852 | + return super(EditEmailAddress, self).checkAuthenticated(user) |
1853 | |
1854 | |
1855 | class ViewGPGKey(AnonymousAuthorization): |
1856 | |
1857 | === modified file 'lib/lp/services/identity/doc/account.txt' |
1858 | --- lib/lp/services/identity/doc/account.txt 2011-12-24 17:49:30 +0000 |
1859 | +++ lib/lp/services/identity/doc/account.txt 2012-01-13 14:12:26 +0000 |
1860 | @@ -13,7 +13,7 @@ |
1861 | implements the IAccountSet interface. |
1862 | |
1863 | >>> from zope.interface.verify import verifyObject |
1864 | - >>> from lp.registry.interfaces.person import IPerson |
1865 | + >>> from lp.registry.interfaces.person import IPersonSet |
1866 | >>> from lp.services.identity.interfaces.account import ( |
1867 | ... IAccount, IAccountSet) |
1868 | |
1869 | @@ -21,37 +21,8 @@ |
1870 | >>> verifyObject(IAccountSet, account_set) |
1871 | True |
1872 | |
1873 | - |
1874 | -Looking up accounts by email address |
1875 | ------------------------------------- |
1876 | - |
1877 | -Accounts are generally looked up by email address. |
1878 | - |
1879 | - >>> login('no-priv@canonical.com') |
1880 | - >>> account = account_set.getByEmail('no-priv@canonical.com') |
1881 | - >>> IAccount.providedBy(account) |
1882 | - True |
1883 | - |
1884 | -If the account is not found, a LookupError is raised. |
1885 | - |
1886 | - >>> account_set.getByEmail('invalid@whatever') |
1887 | - Traceback (most recent call last): |
1888 | - ... |
1889 | - LookupError:... |
1890 | - |
1891 | -Only admins or the person attached to an account can see or edit Account |
1892 | -details. This is obviously wrong, as the account should have access |
1893 | -rather than the (optional) attached person. In particular, it means |
1894 | -Accounts without Person records cannot be managed by the Account owner. |
1895 | -Fixing this involves more surgery to Launchpad's security systems. |
1896 | - |
1897 | - >>> stub_account = account_set.getByEmail('stuart.bishop@canonical.com') |
1898 | - >>> stub_account.date_created |
1899 | - Traceback (most recent call last): |
1900 | - ... |
1901 | - Unauthorized... |
1902 | - |
1903 | - >>> del stub_account |
1904 | + >>> account = getUtility(IPersonSet).getByEmail( |
1905 | + ... 'no-priv@canonical.com').account |
1906 | |
1907 | |
1908 | Looking up accounts by their database ID |
1909 | @@ -102,49 +73,16 @@ |
1910 | True |
1911 | >>> login('no-priv@canonical.com') |
1912 | |
1913 | -An account has a displayname, and a preferred email address. |
1914 | +An account has a displayname. |
1915 | |
1916 | >>> print account.displayname |
1917 | No Privileges Person |
1918 | - >>> print account.preferredemail.email |
1919 | - no-priv@canonical.com |
1920 | |
1921 | Account objects have a useful string representation. |
1922 | |
1923 | >>> account |
1924 | <Account 'No Privileges Person' (Active account)> |
1925 | |
1926 | -The account can have additional validated and guessed email |
1927 | -addresses. This will be empty if the user has only a single validated |
1928 | -email address. |
1929 | - |
1930 | - >>> [email.email for email in account.validated_emails] |
1931 | - [] |
1932 | - >>> [email.email for email in account.guessed_emails] |
1933 | - [] |
1934 | - |
1935 | -If we add a new guessed email address, it will be included in the |
1936 | -guessed list. |
1937 | - |
1938 | - >>> from lp.services.identity.interfaces.emailaddress import ( |
1939 | - ... EmailAddressStatus, |
1940 | - ... IEmailAddressSet, |
1941 | - ... ) |
1942 | - >>> email = getUtility(IEmailAddressSet).new( |
1943 | - ... "guessed-email@example.com", account=account, |
1944 | - ... status=EmailAddressStatus.NEW) |
1945 | - >>> [email.email for email in account.guessed_emails] |
1946 | - [u'guessed-email@example.com'] |
1947 | - |
1948 | -If we add a validated email address, it will show up in the validated |
1949 | -list. |
1950 | - |
1951 | - >>> email = getUtility(IEmailAddressSet).new( |
1952 | - ... "validated-email@example.com", account=account, |
1953 | - ... status=EmailAddressStatus.VALIDATED) |
1954 | - >>> [email.email for email in account.validated_emails] |
1955 | - [u'validated-email@example.com'] |
1956 | - |
1957 | It also has an encrypted password. |
1958 | |
1959 | >>> print account.password |
1960 | @@ -226,8 +164,6 @@ |
1961 | Passwordless |
1962 | >>> print passwordless_account.password |
1963 | None |
1964 | - >>> print passwordless_account.preferredemail |
1965 | - None |
1966 | |
1967 | The new() method accepts the optional parameters of password and |
1968 | password_is_encrypted. If password_is_encrypted is False, the default, |
1969 | @@ -249,95 +185,3 @@ |
1970 | >>> Store.of(clear_account).flush() |
1971 | >>> print clear_account.password |
1972 | clear_password |
1973 | - |
1974 | - |
1975 | -Valid Accounts |
1976 | --------------- |
1977 | - |
1978 | -Like person objects, an account is considered valid if it is in the |
1979 | -active state and has a preferred email address. So a newly created |
1980 | -account with no email address is not valid. |
1981 | - |
1982 | - >>> account = account_set.new( |
1983 | - ... AccountCreationRationale.USER_CREATED, |
1984 | - ... "Valid Account Test") |
1985 | - >>> account.status = AccountStatus.ACTIVE |
1986 | - >>> account.is_valid |
1987 | - False |
1988 | - |
1989 | -Let's add a new email address to the account. |
1990 | - |
1991 | - >>> email = getUtility(IEmailAddressSet).new( |
1992 | - ... "valid-account-test@example.com", account=account) |
1993 | - >>> account.is_valid |
1994 | - False |
1995 | - |
1996 | -The account is still not valid because it has no preferred email. |
1997 | -Setting the email to preferred fixes this. |
1998 | - |
1999 | - >>> from lp.services.identity.interfaces.emailaddress import EmailAddressStatus |
2000 | - >>> email.status = EmailAddressStatus.PREFERRED |
2001 | - >>> account.is_valid |
2002 | - True |
2003 | - |
2004 | -If the account is deactivated, it won't be considered valid any more: |
2005 | - |
2006 | - >>> account.status = AccountStatus.DEACTIVATED |
2007 | - >>> account.is_valid |
2008 | - False |
2009 | - |
2010 | - |
2011 | -Creating an IPerson for an Account |
2012 | ----------------------------------- |
2013 | - |
2014 | -Newly created accounts without an associated Person can be 'promoted' to full |
2015 | -Launchpad accounts with an attached Person. |
2016 | - |
2017 | - # We need to change database policy here again, as the SSO Server cannot |
2018 | - # modify tables in the lpmain replication set. |
2019 | - >>> from lp.services.webapp.dbpolicy import MasterDatabasePolicy |
2020 | - >>> from lp.services.webapp.interfaces import IStoreSelector |
2021 | - >>> getUtility(IStoreSelector).push(MasterDatabasePolicy()) |
2022 | - |
2023 | - >>> from lp.registry.interfaces.person import PersonCreationRationale |
2024 | - >>> fresh_account, email = account_set.createAccountAndEmail( |
2025 | - ... 'foo@example.com', |
2026 | - ... AccountCreationRationale.OWNER_CREATED_UBUNTU_SHOP, |
2027 | - ... 'Display name', 'password') |
2028 | - >>> IPerson(fresh_account) |
2029 | - Traceback (most recent call last): |
2030 | - ... |
2031 | - TypeError: ('Could not adapt', ... |
2032 | - |
2033 | - >>> person = fresh_account.createPerson( |
2034 | - ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD) |
2035 | - >>> transaction.commit() |
2036 | - >>> person.account == fresh_account |
2037 | - True |
2038 | - >>> IPerson(fresh_account) == person |
2039 | - True |
2040 | - >>> person.preferredemail == fresh_account.preferredemail |
2041 | - True |
2042 | - >>> person.creation_rationale |
2043 | - <DBItem PersonCreationRationale.OWNER_CREATED_LAUNCHPAD... |
2044 | - |
2045 | -However, if the account has an associated person or has no preferred email |
2046 | -address, a new Person cannot be created. |
2047 | - |
2048 | - >>> person = fresh_account.createPerson( |
2049 | - ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD) |
2050 | - Traceback (most recent call last): |
2051 | - ... |
2052 | - AssertionError: Can't create a Person for an account which already has |
2053 | - one. |
2054 | - |
2055 | - >>> print clear_account.preferredemail |
2056 | - None |
2057 | - >>> person = clear_account.createPerson( |
2058 | - ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD) |
2059 | - Traceback (most recent call last): |
2060 | - ... |
2061 | - AssertionError: Can't create a Person for an account which has no email. |
2062 | - |
2063 | - >>> db_policy = getUtility(IStoreSelector).pop() |
2064 | - |
2065 | |
2066 | === modified file 'lib/lp/services/identity/interfaces/account.py' |
2067 | --- lib/lp/services/identity/interfaces/account.py 2011-12-24 16:54:44 +0000 |
2068 | +++ lib/lp/services/identity/interfaces/account.py 2012-01-13 14:12:26 +0000 |
2069 | @@ -23,21 +23,15 @@ |
2070 | DBEnumeratedType, |
2071 | DBItem, |
2072 | ) |
2073 | -from lazr.restful.fields import ( |
2074 | - CollectionField, |
2075 | - Reference, |
2076 | - ) |
2077 | from zope.interface import ( |
2078 | Attribute, |
2079 | Interface, |
2080 | ) |
2081 | from zope.schema import ( |
2082 | - Bool, |
2083 | Choice, |
2084 | Datetime, |
2085 | Int, |
2086 | Text, |
2087 | - TextLine, |
2088 | ) |
2089 | |
2090 | from lp import _ |
2091 | @@ -223,58 +217,6 @@ |
2092 | title=_("The status of this account"), required=True, |
2093 | readonly=False, vocabulary=AccountStatus) |
2094 | |
2095 | - is_valid = Bool( |
2096 | - title=_("True if this account is active and has a valid email."), |
2097 | - required=True, readonly=True) |
2098 | - |
2099 | - # We should use schema=IEmailAddress here, but we can't because that would |
2100 | - # cause circular dependencies. |
2101 | - preferredemail = Reference( |
2102 | - title=_("Preferred email address"), |
2103 | - description=_("The preferred email address for this person. " |
2104 | - "The one we'll use to communicate with them."), |
2105 | - readonly=True, required=False, schema=Interface) |
2106 | - |
2107 | - validated_emails = CollectionField( |
2108 | - title=_("Confirmed e-mails of this account."), |
2109 | - description=_( |
2110 | - "Confirmed e-mails are the ones in the VALIDATED state. The " |
2111 | - "user has confirmed that they are active and that they control " |
2112 | - "them."), |
2113 | - readonly=True, required=False, |
2114 | - value_type=Reference(schema=Interface)) |
2115 | - |
2116 | - guessed_emails = CollectionField( |
2117 | - title=_("Guessed e-mails of this account."), |
2118 | - description=_( |
2119 | - "Guessed e-mails are the ones in the NEW state. We believe " |
2120 | - "that the user owns the address, but they have not confirmed " |
2121 | - "the fact."), |
2122 | - readonly=True, required=False, |
2123 | - value_type=Reference(schema=Interface)) |
2124 | - |
2125 | - def setPreferredEmail(email): |
2126 | - """Set the given email address as this account's preferred one. |
2127 | - |
2128 | - If ``email`` is None, the preferred email address is unset, which |
2129 | - will make the account invalid. |
2130 | - """ |
2131 | - |
2132 | - def validateAndEnsurePreferredEmail(email): |
2133 | - """Ensure this account has a preferred email. |
2134 | - |
2135 | - If this account doesn't have a preferred email, <email> will be set as |
2136 | - this account's preferred one. Otherwise it'll be set as VALIDATED and |
2137 | - this account will keep their old preferred email. |
2138 | - |
2139 | - This method is meant to be the only one to change the status of an |
2140 | - email address, but as we all know the real world is far from ideal |
2141 | - and we have to deal with this in one more place, which is the case |
2142 | - when people explicitly want to change their preferred email address. |
2143 | - On that case, though, all we have to do is use |
2144 | - account.setPreferredEmail(). |
2145 | - """ |
2146 | - |
2147 | |
2148 | class IAccountPrivate(Interface): |
2149 | """Private information on an `IAccount`.""" |
2150 | @@ -290,16 +232,6 @@ |
2151 | password = PasswordField( |
2152 | title=_("Password."), readonly=False, required=True) |
2153 | |
2154 | - def createPerson(rationale, name=None, comment=None): |
2155 | - """Create and return a new `IPerson` associated with this account. |
2156 | - |
2157 | - :param rationale: A member of `AccountCreationRationale`. |
2158 | - :param name: Specify a name for the `IPerson` instead of |
2159 | - using an automatically generated one. |
2160 | - :param comment: Populate `IPerson.creation_comment`. See |
2161 | - `IPerson`. |
2162 | - """ |
2163 | - |
2164 | |
2165 | class IAccountSpecialRestricted(Interface): |
2166 | """Attributes of `IAccount` protected with launchpad.Special.""" |
2167 | @@ -312,12 +244,7 @@ |
2168 | title=_("Why are you deactivating your account?"), |
2169 | required=False, readonly=False) |
2170 | |
2171 | - # XXX sinzui 2008-07-14 bug=248518: |
2172 | - # This method would assert the password is not None, but |
2173 | - # setPreferredEmail() passes the Person's current password, which may |
2174 | - # be None. Once that callsite is fixed, we will be able to check that the |
2175 | - # password is not None here and get rid of the reactivate() method below. |
2176 | - def activate(comment, password, preferred_email): |
2177 | + def reactivate(comment, password): |
2178 | """Activate this account. |
2179 | |
2180 | Set the account status to ACTIVE, the account's password to the given |
2181 | @@ -325,15 +252,6 @@ |
2182 | |
2183 | :param comment: An explanation of why the account status changed. |
2184 | :param password: The user's password. |
2185 | - :param preferred_email: The `EmailAddress` to set as the account's |
2186 | - preferred email address. It cannot be None. |
2187 | - """ |
2188 | - |
2189 | - def reactivate(comment, password, preferred_email): |
2190 | - """Reactivate this account. |
2191 | - |
2192 | - Just like `IAccountSpecialRestricted`.activate() above, but here the |
2193 | - password can't be None or the empty string. |
2194 | """ |
2195 | |
2196 | |
2197 | @@ -364,24 +282,6 @@ |
2198 | :raises LookupError: If the account is not found. |
2199 | """ |
2200 | |
2201 | - def createAccountAndEmail(email, rationale, displayname, password, |
2202 | - password_is_encrypted=False): |
2203 | - """Create and return both a new `IAccount` and `IEmailAddress`. |
2204 | - |
2205 | - The account will be in the ACTIVE state, with the email address set as |
2206 | - its preferred email address. |
2207 | - """ |
2208 | - |
2209 | - def getByEmail(email): |
2210 | - """Return the `IAccount` linked to the given email address. |
2211 | - |
2212 | - :param email: A string, not an `IEmailAddress` provider. |
2213 | - |
2214 | - :return: An `IAccount`. |
2215 | - |
2216 | - :raises LookupError: If the account is not found. |
2217 | - """ |
2218 | - |
2219 | def getByOpenIDIdentifier(openid_identity): |
2220 | """Return the `IAccount` with the given OpenID identifier. |
2221 | |
2222 | @@ -390,4 +290,3 @@ |
2223 | :return: An `IAccount` |
2224 | :raises LookupError: If the account is not found. |
2225 | """ |
2226 | - |
2227 | |
2228 | === modified file 'lib/lp/services/identity/interfaces/emailaddress.py' |
2229 | --- lib/lp/services/identity/interfaces/emailaddress.py 2012-01-05 00:15:32 +0000 |
2230 | +++ lib/lp/services/identity/interfaces/emailaddress.py 2012-01-13 14:12:26 +0000 |
2231 | @@ -32,7 +32,6 @@ |
2232 | |
2233 | from lp import _ |
2234 | from lp.registry.interfaces.role import IHasOwner |
2235 | -from lp.services.identity.interfaces.account import IAccount |
2236 | |
2237 | |
2238 | class InvalidEmailAddress(Exception): |
2239 | @@ -99,8 +98,6 @@ |
2240 | status = Choice( |
2241 | title=_('Email Address Status'), required=True, readonly=False, |
2242 | vocabulary=EmailAddressStatus) |
2243 | - account = Object(title=_('Account'), schema=IAccount, required=False) |
2244 | - accountID = Int(title=_('AccountID'), required=False, readonly=True) |
2245 | person = exported( |
2246 | Reference(title=_('Person'), required=False, readonly=False, |
2247 | schema=Interface)) |
2248 | @@ -131,12 +128,10 @@ |
2249 | class IEmailAddressSet(Interface): |
2250 | """The set of EmailAddresses.""" |
2251 | |
2252 | - def new(email, person=None, status=EmailAddressStatus.NEW, account=None): |
2253 | + def new(email, person=None, status=EmailAddressStatus.NEW): |
2254 | """Create a new EmailAddress with the given email. |
2255 | |
2256 | - The newly created EmailAddress will point to the person |
2257 | - and/or account. If account is omitted and the person has a linked |
2258 | - account, that account will be used. |
2259 | + The newly created EmailAddress will point to the person. |
2260 | |
2261 | The given status must be an item of EmailAddressStatus. |
2262 | |
2263 | |
2264 | === modified file 'lib/lp/services/identity/model/account.py' |
2265 | --- lib/lp/services/identity/model/account.py 2011-12-30 06:14:56 +0000 |
2266 | +++ lib/lp/services/identity/model/account.py 2012-01-13 14:12:26 +0000 |
2267 | @@ -15,16 +15,13 @@ |
2268 | StringCol, |
2269 | ) |
2270 | from storm.locals import ReferenceSet |
2271 | -from storm.store import Store |
2272 | from zope.component import getUtility |
2273 | from zope.interface import implements |
2274 | -from zope.security.proxy import removeSecurityProxy |
2275 | |
2276 | from lp.services.database.constants import UTC_NOW |
2277 | from lp.services.database.datetimecol import UtcDateTimeCol |
2278 | from lp.services.database.enumcol import EnumCol |
2279 | from lp.services.database.lpstorm import ( |
2280 | - IMasterObject, |
2281 | IMasterStore, |
2282 | IStore, |
2283 | ) |
2284 | @@ -35,12 +32,6 @@ |
2285 | IAccount, |
2286 | IAccountSet, |
2287 | ) |
2288 | -from lp.services.identity.interfaces.emailaddress import ( |
2289 | - EmailAddressStatus, |
2290 | - IEmailAddress, |
2291 | - IEmailAddressSet, |
2292 | - ) |
2293 | -from lp.services.identity.model.emailaddress import EmailAddress |
2294 | from lp.services.openid.model.openididentifier import OpenIdIdentifier |
2295 | from lp.services.webapp.interfaces import IPasswordEncryptor |
2296 | |
2297 | @@ -70,100 +61,11 @@ |
2298 | return "<%s '%s' (%s)>" % ( |
2299 | self.__class__.__name__, displayname, self.status) |
2300 | |
2301 | - def _getEmails(self, status): |
2302 | - """Get related `EmailAddress` objects with the given status.""" |
2303 | - result = IStore(EmailAddress).find( |
2304 | - EmailAddress, accountID=self.id, status=status) |
2305 | - result.order_by(EmailAddress.email.lower()) |
2306 | - return result |
2307 | - |
2308 | - @property |
2309 | - def preferredemail(self): |
2310 | - """See `IAccount`.""" |
2311 | - return self._getEmails(EmailAddressStatus.PREFERRED).one() |
2312 | - |
2313 | - @property |
2314 | - def validated_emails(self): |
2315 | - """See `IAccount`.""" |
2316 | - return self._getEmails(EmailAddressStatus.VALIDATED) |
2317 | - |
2318 | - @property |
2319 | - def guessed_emails(self): |
2320 | - """See `IAccount`.""" |
2321 | - return self._getEmails(EmailAddressStatus.NEW) |
2322 | - |
2323 | - def setPreferredEmail(self, email): |
2324 | - """See `IAccount`.""" |
2325 | - if email is None: |
2326 | - # Mark preferred email address as validated, if it exists. |
2327 | - # XXX 2009-03-30 jamesh bug=349482: we should be able to |
2328 | - # use ResultSet.set() here :( |
2329 | - for address in self._getEmails(EmailAddressStatus.PREFERRED): |
2330 | - address.status = EmailAddressStatus.VALIDATED |
2331 | - return |
2332 | - |
2333 | - if not IEmailAddress.providedBy(email): |
2334 | - raise TypeError("Any person's email address must provide the " |
2335 | - "IEmailAddress Interface. %r doesn't." % email) |
2336 | - |
2337 | - email = IMasterObject(removeSecurityProxy(email)) |
2338 | - assert email.accountID == self.id |
2339 | - |
2340 | - # If we have the preferred email address here, we're done. |
2341 | - if email.status == EmailAddressStatus.PREFERRED: |
2342 | - return |
2343 | - |
2344 | - existing_preferred_email = self.preferredemail |
2345 | - if existing_preferred_email is not None: |
2346 | - assert Store.of(email) is Store.of(existing_preferred_email), ( |
2347 | - "Store of %r is not the same as store of %r" % |
2348 | - (email, existing_preferred_email)) |
2349 | - existing_preferred_email.status = EmailAddressStatus.VALIDATED |
2350 | - # Make sure the old preferred email gets flushed before |
2351 | - # setting the new preferred email. |
2352 | - Store.of(email).add_flush_order(existing_preferred_email, email) |
2353 | - |
2354 | - email.status = EmailAddressStatus.PREFERRED |
2355 | - |
2356 | - def validateAndEnsurePreferredEmail(self, email): |
2357 | - """See `IAccount`.""" |
2358 | - if not IEmailAddress.providedBy(email): |
2359 | - raise TypeError( |
2360 | - "Any person's email address must provide the IEmailAddress " |
2361 | - "interface. %s doesn't." % email) |
2362 | - |
2363 | - assert email.accountID == self.id, 'Wrong account! %r, %r' % ( |
2364 | - email.accountID, self.id) |
2365 | - |
2366 | - # This email is already validated and is this person's preferred |
2367 | - # email, so we have nothing to do. |
2368 | - if email.status == EmailAddressStatus.PREFERRED: |
2369 | - return |
2370 | - |
2371 | - email = IMasterObject(email) |
2372 | - |
2373 | - if self.preferredemail is None: |
2374 | - # This branch will be executed only in the first time a person |
2375 | - # uses Launchpad. Either when creating a new account or when |
2376 | - # resetting the password of an automatically created one. |
2377 | - self.setPreferredEmail(email) |
2378 | - else: |
2379 | - email.status = EmailAddressStatus.VALIDATED |
2380 | - |
2381 | - def activate(self, comment, password, preferred_email): |
2382 | + def reactivate(self, comment, password): |
2383 | """See `IAccountSpecialRestricted`.""" |
2384 | - if preferred_email is None: |
2385 | - raise AssertionError( |
2386 | - "Account %s cannot be activated without a " |
2387 | - "preferred email address." % self.id) |
2388 | self.status = AccountStatus.ACTIVE |
2389 | self.status_comment = comment |
2390 | self.password = password |
2391 | - self.validateAndEnsurePreferredEmail(preferred_email) |
2392 | - |
2393 | - def reactivate(self, comment, password, preferred_email): |
2394 | - """See `IAccountSpecialRestricted`.""" |
2395 | - self.activate(comment, password, preferred_email) |
2396 | |
2397 | # The password is actually stored in a separate table for security |
2398 | # reasons, so use a property to hide this implementation detail. |
2399 | @@ -201,39 +103,6 @@ |
2400 | |
2401 | password = property(_get_password, _set_password) |
2402 | |
2403 | - @property |
2404 | - def is_valid(self): |
2405 | - """See `IAccount`.""" |
2406 | - if self.status != AccountStatus.ACTIVE: |
2407 | - return False |
2408 | - return self.preferredemail is not None |
2409 | - |
2410 | - def createPerson(self, rationale, name=None, comment=None): |
2411 | - """See `IAccount`.""" |
2412 | - # Need a local import because of circular dependencies. |
2413 | - from lp.registry.model.person import ( |
2414 | - generate_nick, Person, PersonSet) |
2415 | - assert self.preferredemail is not None, ( |
2416 | - "Can't create a Person for an account which has no email.") |
2417 | - person = IMasterStore(Person).find(Person, accountID=self.id).one() |
2418 | - assert person is None, ( |
2419 | - "Can't create a Person for an account which already has one.") |
2420 | - if name is None: |
2421 | - name = generate_nick(self.preferredemail.email) |
2422 | - person = PersonSet()._newPerson( |
2423 | - name, self.displayname, hide_email_addresses=True, |
2424 | - rationale=rationale, account=self, comment=comment) |
2425 | - |
2426 | - # Update all associated email addresses to point at the new person. |
2427 | - result = IMasterStore(EmailAddress).find( |
2428 | - EmailAddress, accountID=self.id) |
2429 | - # XXX 2009-03-30 jamesh bug=349482: we should be able to |
2430 | - # use ResultSet.set() here :( |
2431 | - for email in result: |
2432 | - email.personID = person.id |
2433 | - |
2434 | - return person |
2435 | - |
2436 | |
2437 | class AccountSet: |
2438 | """See `IAccountSet`.""" |
2439 | @@ -269,39 +138,6 @@ |
2440 | raise LookupError(id) |
2441 | return account |
2442 | |
2443 | - def createAccountAndEmail(self, email, rationale, displayname, password, |
2444 | - password_is_encrypted=False, |
2445 | - openid_identifier=None): |
2446 | - """See `IAccountSet`.""" |
2447 | - # Convert the PersonCreationRationale to an AccountCreationRationale. |
2448 | - account_rationale = getattr(AccountCreationRationale, rationale.name) |
2449 | - account = self.new( |
2450 | - account_rationale, displayname, password=password, |
2451 | - password_is_encrypted=password_is_encrypted, |
2452 | - openid_identifier=openid_identifier) |
2453 | - account.status = AccountStatus.ACTIVE |
2454 | - email = getUtility(IEmailAddressSet).new( |
2455 | - email, status=EmailAddressStatus.PREFERRED, account=account) |
2456 | - return account, email |
2457 | - |
2458 | - def getByEmail(self, email): |
2459 | - """See `IAccountSet`.""" |
2460 | - store = IStore(Account) |
2461 | - try: |
2462 | - email = email.decode('US-ASCII') |
2463 | - except (UnicodeDecodeError, UnicodeEncodeError): |
2464 | - # Non-ascii email addresses are not legal, so assume there are no |
2465 | - # matching addresses in Launchpad. |
2466 | - raise LookupError(repr(email)) |
2467 | - account = store.find( |
2468 | - Account, |
2469 | - EmailAddress.account == Account.id, |
2470 | - EmailAddress.email.lower() |
2471 | - == email.strip().lower()).one() |
2472 | - if account is None: |
2473 | - raise LookupError(email) |
2474 | - return account |
2475 | - |
2476 | def getByOpenIDIdentifier(self, openid_identifier): |
2477 | """See `IAccountSet`.""" |
2478 | store = IStore(Account) |
2479 | |
2480 | === modified file 'lib/lp/services/identity/model/emailaddress.py' |
2481 | --- lib/lp/services/identity/model/emailaddress.py 2012-01-05 00:15:32 +0000 |
2482 | +++ lib/lp/services/identity/model/emailaddress.py 2012-01-13 14:12:26 +0000 |
2483 | @@ -56,9 +56,6 @@ |
2484 | dbName='email', notNull=True, unique=True, alternateID=True) |
2485 | status = EnumCol(dbName='status', schema=EmailAddressStatus, notNull=True) |
2486 | person = ForeignKey(dbName='person', foreignKey='Person', notNull=False) |
2487 | - account = ForeignKey( |
2488 | - dbName='account', foreignKey='Account', notNull=False, |
2489 | - default=None) |
2490 | |
2491 | def __repr__(self): |
2492 | return '<EmailAddress at 0x%x <%s> [%s]>' % ( |
2493 | @@ -116,8 +113,7 @@ |
2494 | return EmailAddress.selectOne( |
2495 | "lower(email) = %s" % quote(email.strip().lower())) |
2496 | |
2497 | - def new(self, email, person=None, status=EmailAddressStatus.NEW, |
2498 | - account=None): |
2499 | + def new(self, email, person=None, status=EmailAddressStatus.NEW): |
2500 | """See IEmailAddressSet.""" |
2501 | email = email.strip() |
2502 | |
2503 | @@ -129,26 +125,11 @@ |
2504 | raise EmailAddressAlreadyTaken( |
2505 | "The email address '%s' is already registered." % email) |
2506 | assert status in EmailAddressStatus.items |
2507 | - if person is None: |
2508 | - personID = None |
2509 | - else: |
2510 | - if account is None: |
2511 | - account = person.account |
2512 | - personID = person.id |
2513 | - accountID = account and account.id |
2514 | - assert person.accountID == accountID, ( |
2515 | - "Email address '%s' must be linked to same account as " |
2516 | - "person '%s'. Expected %r (%s), got %r (%s)" % ( |
2517 | - email, person.name, person.account, person.accountID, |
2518 | - account, accountID)) |
2519 | - # We use personID instead of just person, as in some cases the |
2520 | - # Person record will not yet be replicated from the main |
2521 | - # Store to the auth master Store. |
2522 | + assert person |
2523 | return EmailAddress( |
2524 | email=email, |
2525 | status=status, |
2526 | - personID=personID, |
2527 | - account=account) |
2528 | + person=person) |
2529 | |
2530 | |
2531 | class UndeletableEmailAddress(Exception): |
2532 | |
2533 | === modified file 'lib/lp/services/identity/tests/test_account.py' |
2534 | --- lib/lp/services/identity/tests/test_account.py 2012-01-01 02:58:52 +0000 |
2535 | +++ lib/lp/services/identity/tests/test_account.py 2012-01-13 14:12:26 +0000 |
2536 | @@ -6,25 +6,7 @@ |
2537 | __metaclass__ = type |
2538 | __all__ = [] |
2539 | |
2540 | -from testtools.testcase import ExpectedException |
2541 | -import transaction |
2542 | -from zope.component import getUtility |
2543 | - |
2544 | -from lp.registry.interfaces.person import ( |
2545 | - IPerson, |
2546 | - PersonCreationRationale, |
2547 | - ) |
2548 | -from lp.services.identity.interfaces.account import ( |
2549 | - AccountCreationRationale, |
2550 | - IAccountSet, |
2551 | - ) |
2552 | -from lp.services.identity.interfaces.emailaddress import EmailAddressStatus |
2553 | -from lp.services.webapp.authorization import check_permission |
2554 | -from lp.testing import ( |
2555 | - ANONYMOUS, |
2556 | - login, |
2557 | - TestCaseWithFactory, |
2558 | - ) |
2559 | +from lp.testing import TestCaseWithFactory |
2560 | from lp.testing.layers import DatabaseFunctionalLayer |
2561 | |
2562 | |
2563 | @@ -44,225 +26,3 @@ |
2564 | distro = self.factory.makeAccount(u'\u0170-account') |
2565 | ignore, displayname, status_1, status_2 = repr(distro).rsplit(' ', 3) |
2566 | self.assertEqual("'\\u0170-account'", displayname) |
2567 | - |
2568 | - |
2569 | -class TestPersonlessAccountPermissions(TestCaseWithFactory): |
2570 | - """In order for Person-less accounts to see their non-public details and |
2571 | - email addresses, we had to change the security adapters for IAccount and |
2572 | - IEmailAddress to accept the 'user' argument being either a Person or an |
2573 | - Account. |
2574 | - |
2575 | - Here we login() with one of these person-less accounts and show that they |
2576 | - can see their details, including email addresses. |
2577 | - """ |
2578 | - layer = DatabaseFunctionalLayer |
2579 | - |
2580 | - def setUp(self): |
2581 | - TestCaseWithFactory.setUp(self, 'no-priv@canonical.com') |
2582 | - self.email = 'test@example.com' |
2583 | - self.account = self.factory.makeAccount( |
2584 | - 'Test account, without a person', email=self.email) |
2585 | - |
2586 | - def test_can_view_their_emails(self): |
2587 | - login(self.email) |
2588 | - self.failUnless( |
2589 | - check_permission('launchpad.View', self.account.preferredemail)) |
2590 | - |
2591 | - def test_can_view_their_own_details(self): |
2592 | - login(self.email) |
2593 | - self.failUnless(check_permission('launchpad.View', self.account)) |
2594 | - |
2595 | - def test_can_change_their_own_details(self): |
2596 | - login(self.email) |
2597 | - self.failUnless(check_permission('launchpad.Edit', self.account)) |
2598 | - |
2599 | - def test_emails_of_personless_acounts_cannot_be_seen_by_others(self): |
2600 | - # Email addresses are visible to others only when the user has |
2601 | - # explicitly chosen to have them shown, and that state is stored in |
2602 | - # IPerson.hide_email_addresses, so for accounts that have no |
2603 | - # associated Person, we will hide the email addresses from others. |
2604 | - login('no-priv@canonical.com') |
2605 | - self.failIf(check_permission( |
2606 | - 'launchpad.View', self.account.preferredemail)) |
2607 | - |
2608 | - # Anonymous users can't see them either. |
2609 | - login(ANONYMOUS) |
2610 | - self.failIf(check_permission( |
2611 | - 'launchpad.View', self.account.preferredemail)) |
2612 | - |
2613 | - |
2614 | -class CreatePersonTests(TestCaseWithFactory): |
2615 | - """Tests for `IAccount.createPerson`.""" |
2616 | - |
2617 | - layer = DatabaseFunctionalLayer |
2618 | - |
2619 | - def setUp(self): |
2620 | - super(CreatePersonTests, self).setUp(user='admin@canonical.com') |
2621 | - |
2622 | - def test_createPerson(self): |
2623 | - account = self.factory.makeAccount("Test Account") |
2624 | - # Account has no person. |
2625 | - self.assertEqual(IPerson(account, None), None) |
2626 | - self.assertEqual(account.preferredemail.person, None) |
2627 | - |
2628 | - person = account.createPerson(PersonCreationRationale.UNKNOWN) |
2629 | - transaction.commit() |
2630 | - self.assertNotEqual(person, None) |
2631 | - self.assertEqual(person.account, account) |
2632 | - self.assertEqual(IPerson(account), person) |
2633 | - self.assertEqual(account.preferredemail.person, person) |
2634 | - |
2635 | - # Trying to create a person for an account with a person fails. |
2636 | - self.assertRaises(AssertionError, account.createPerson, |
2637 | - PersonCreationRationale.UNKNOWN) |
2638 | - |
2639 | - def test_createPerson_requires_email(self): |
2640 | - # It isn't possible to create a person for an account with no |
2641 | - # preferred email address. |
2642 | - account = getUtility(IAccountSet).new( |
2643 | - AccountCreationRationale.UNKNOWN, "Test Account") |
2644 | - self.assertEqual(account.preferredemail, None) |
2645 | - self.assertRaises(AssertionError, account.createPerson, |
2646 | - PersonCreationRationale.UNKNOWN) |
2647 | - |
2648 | - def test_createPerson_sets_EmailAddress_person(self): |
2649 | - # All email addresses for the account are associated with the |
2650 | - # new person |
2651 | - account = self.factory.makeAccount("Test Account") |
2652 | - valid_email = self.factory.makeEmail( |
2653 | - "validated@example.org", None, account, |
2654 | - EmailAddressStatus.VALIDATED) |
2655 | - new_email = self.factory.makeEmail( |
2656 | - "new@example.org", None, account, |
2657 | - EmailAddressStatus.NEW) |
2658 | - old_email = self.factory.makeEmail( |
2659 | - "old@example.org", None, account, |
2660 | - EmailAddressStatus.OLD) |
2661 | - |
2662 | - person = account.createPerson(PersonCreationRationale.UNKNOWN) |
2663 | - transaction.commit() |
2664 | - self.assertEqual(valid_email.person, person) |
2665 | - self.assertEqual(new_email.person, person) |
2666 | - self.assertEqual(old_email.person, person) |
2667 | - |
2668 | - def test_createPerson_uses_name(self): |
2669 | - # A optional user name can be provided. Normally the name is |
2670 | - # generated from the email address. |
2671 | - account = self.factory.makeAccount("Test Account") |
2672 | - person = account.createPerson( |
2673 | - PersonCreationRationale.UNKNOWN, name="sam.bell") |
2674 | - self.failUnlessEqual("sam.bell", person.name) |
2675 | - |
2676 | - def test_createPerson_uses_comment(self): |
2677 | - # An optional creation comment can be provided. |
2678 | - account = self.factory.makeAccount("Test Account") |
2679 | - person = account.createPerson( |
2680 | - PersonCreationRationale.UNKNOWN, |
2681 | - comment="when importing He-3 from the Moon") |
2682 | - self.failUnlessEqual( |
2683 | - "when importing He-3 from the Moon", |
2684 | - person.creation_comment) |
2685 | - |
2686 | - def test_getByEmail_non_ascii_bytes(self): |
2687 | - """Lookups for non-ascii addresses should raise LookupError. |
2688 | - |
2689 | - This tests the case where input is a bytestring. |
2690 | - """ |
2691 | - with ExpectedException(LookupError, r"'SaraS\\xe1nchez@cocolee.net'"): |
2692 | - getUtility(IAccountSet).getByEmail('SaraS\xe1nchez@cocolee.net') |
2693 | - |
2694 | - def test_getByEmail_non_ascii_unicode(self): |
2695 | - """Lookups for non-ascii addresses should raise LookupError. |
2696 | - |
2697 | - This tests the case where input is a unicode string. |
2698 | - """ |
2699 | - with ExpectedException(LookupError, r"u'SaraS\\xe1nchez@.*.net'"): |
2700 | - getUtility(IAccountSet).getByEmail(u'SaraS\xe1nchez@cocolee.net') |
2701 | - |
2702 | - |
2703 | -class EmailManagementTests(TestCaseWithFactory): |
2704 | - """Test email account management interfaces for `IAccount`.""" |
2705 | - |
2706 | - layer = DatabaseFunctionalLayer |
2707 | - |
2708 | - def setUp(self): |
2709 | - super(EmailManagementTests, self).setUp(user='admin@canonical.com') |
2710 | - |
2711 | - def test_setPreferredEmail(self): |
2712 | - # Setting a new preferred email marks the old one as VALIDATED. |
2713 | - account = self.factory.makeAccount("Test Account") |
2714 | - first_email = account.preferredemail |
2715 | - second_email = self.factory.makeEmail( |
2716 | - "second-email@example.org", None, account, |
2717 | - EmailAddressStatus.VALIDATED) |
2718 | - transaction.commit() |
2719 | - account.setPreferredEmail(second_email) |
2720 | - transaction.commit() |
2721 | - self.assertEqual(account.preferredemail, second_email) |
2722 | - self.assertEqual(second_email.status, EmailAddressStatus.PREFERRED) |
2723 | - self.assertEqual(first_email.status, EmailAddressStatus.VALIDATED) |
2724 | - |
2725 | - def test_setPreferredEmail_None(self): |
2726 | - # Setting the preferred email to None sets the old preferred |
2727 | - # email to VALIDATED. |
2728 | - account = self.factory.makeAccount("Test Account") |
2729 | - email = account.preferredemail |
2730 | - transaction.commit() |
2731 | - account.setPreferredEmail(None) |
2732 | - transaction.commit() |
2733 | - self.assertEqual(account.preferredemail, None) |
2734 | - self.assertEqual(email.status, EmailAddressStatus.VALIDATED) |
2735 | - |
2736 | - def test_validateAndEnsurePreferredEmail(self): |
2737 | - # validateAndEnsurePreferredEmail() sets the email status to |
2738 | - # VALIDATED if there is no existing preferred email. |
2739 | - account = self.factory.makeAccount("Test Account") |
2740 | - self.assertNotEqual(account.preferredemail, None) |
2741 | - new_email = self.factory.makeEmail( |
2742 | - "new-email@example.org", None, account, |
2743 | - EmailAddressStatus.NEW) |
2744 | - transaction.commit() |
2745 | - account.validateAndEnsurePreferredEmail(new_email) |
2746 | - transaction.commit() |
2747 | - self.assertEqual(new_email.status, EmailAddressStatus.VALIDATED) |
2748 | - |
2749 | - def test_validateAndEsnurePreferredEmail_no_preferred(self): |
2750 | - # validateAndEnsurePreferredEmail() sets the new email as |
2751 | - # preferred if there was no preferred email. |
2752 | - account = self.factory.makeAccount("Test Account") |
2753 | - account.setPreferredEmail(None) |
2754 | - new_email = self.factory.makeEmail( |
2755 | - "new-email@example.org", None, account, |
2756 | - EmailAddressStatus.NEW) |
2757 | - transaction.commit() |
2758 | - account.validateAndEnsurePreferredEmail(new_email) |
2759 | - transaction.commit() |
2760 | - self.assertEqual(new_email.status, EmailAddressStatus.PREFERRED) |
2761 | - |
2762 | - def test_validated_emails(self): |
2763 | - account = self.factory.makeAccount("Test Account") |
2764 | - self.factory.makeEmail( |
2765 | - "new-email@example.org", None, account, |
2766 | - EmailAddressStatus.NEW) |
2767 | - validated_email = self.factory.makeEmail( |
2768 | - "validated-email@example.org", None, account, |
2769 | - EmailAddressStatus.VALIDATED) |
2770 | - self.factory.makeEmail( |
2771 | - "old@example.org", None, account, |
2772 | - EmailAddressStatus.OLD) |
2773 | - transaction.commit() |
2774 | - self.assertContentEqual(account.validated_emails, [validated_email]) |
2775 | - |
2776 | - def test_guessed_emails(self): |
2777 | - account = self.factory.makeAccount("Test Account") |
2778 | - new_email = self.factory.makeEmail( |
2779 | - "new-email@example.org", None, account, |
2780 | - EmailAddressStatus.NEW) |
2781 | - self.factory.makeEmail( |
2782 | - "validated-email@example.org", None, account, |
2783 | - EmailAddressStatus.VALIDATED) |
2784 | - self.factory.makeEmail( |
2785 | - "old@example.org", None, account, |
2786 | - EmailAddressStatus.OLD) |
2787 | - transaction.commit() |
2788 | - self.assertContentEqual(account.guessed_emails, [new_email]) |
2789 | |
2790 | === modified file 'lib/lp/services/mail/incoming.py' |
2791 | --- lib/lp/services/mail/incoming.py 2011-12-30 02:24:09 +0000 |
2792 | +++ lib/lp/services/mail/incoming.py 2012-01-13 14:12:26 +0000 |
2793 | @@ -225,13 +225,7 @@ |
2794 | setupInteraction(authutil.unauthenticatedPrincipal()) |
2795 | return None |
2796 | |
2797 | - # People with accounts but no related person will have a principal, but |
2798 | - # the person adaptation will fail. |
2799 | person = IPerson(principal, None) |
2800 | - if person is None: |
2801 | - setupInteraction(authutil.unauthenticatedPrincipal()) |
2802 | - return None |
2803 | - |
2804 | if person.account_status != AccountStatus.ACTIVE: |
2805 | raise InactiveAccount( |
2806 | "Mail from a user with an inactive account.") |
2807 | |
2808 | === modified file 'lib/lp/services/mail/tests/test_incoming.py' |
2809 | --- lib/lp/services/mail/tests/test_incoming.py 2012-01-01 02:58:52 +0000 |
2810 | +++ lib/lp/services/mail/tests/test_incoming.py 2012-01-13 14:12:26 +0000 |
2811 | @@ -104,13 +104,6 @@ |
2812 | mail = self.factory.makeSignedMessage(email_address=unknown) |
2813 | self.assertThat(authenticateEmail(mail), Is(None)) |
2814 | |
2815 | - def test_accounts_without_person(self): |
2816 | - # An account without a person should be the same as an unknown email. |
2817 | - email = 'non-person@example.com' |
2818 | - self.factory.makeAccount(email=email) |
2819 | - mail = self.factory.makeSignedMessage(email_address=email) |
2820 | - self.assertThat(authenticateEmail(mail), Is(None)) |
2821 | - |
2822 | |
2823 | class TestExtractAddresses(TestCaseWithFactory): |
2824 | |
2825 | |
2826 | === modified file 'lib/lp/services/verification/browser/logintoken.py' |
2827 | --- lib/lp/services/verification/browser/logintoken.py 2012-01-05 00:15:32 +0000 |
2828 | +++ lib/lp/services/verification/browser/logintoken.py 2012-01-13 14:12:26 +0000 |
2829 | @@ -441,13 +441,11 @@ |
2830 | validated = ( |
2831 | EmailAddressStatus.VALIDATED, EmailAddressStatus.PREFERRED) |
2832 | requester = self.context.requester |
2833 | - account = requester.account |
2834 | |
2835 | emailset = getUtility(IEmailAddressSet) |
2836 | email = emailset.getByEmail(self.context.email) |
2837 | if email is not None: |
2838 | - if email.personID is not None and ( |
2839 | - requester is None or email.personID != requester.id): |
2840 | + if requester is None or email.personID != requester.id: |
2841 | dupe = email.person |
2842 | dname = cgi.escape(dupe.name) |
2843 | # Yes, hardcoding an autogenerated field name is an evil |
2844 | @@ -463,12 +461,6 @@ |
2845 | 'case you should be able to <a href="${url}">merge them' |
2846 | '</a> into a single one.', |
2847 | mapping=dict(url=url)))) |
2848 | - elif account is not None and email.accountID != account.id: |
2849 | - # Email address is owned by a personless account. We |
2850 | - # can't offer to perform a merge here. |
2851 | - self.addError( |
2852 | - 'This email address is already registered for another ' |
2853 | - 'account') |
2854 | elif email.status in validated: |
2855 | self.addError(_( |
2856 | "This email address is already registered and validated " |
2857 | @@ -523,7 +515,7 @@ |
2858 | |
2859 | def markEmailAsValid(self, email): |
2860 | """Mark the given email address as valid.""" |
2861 | - self.context.requester.account.validateAndEnsurePreferredEmail(email) |
2862 | + self.context.requester.validateAndEnsurePreferredEmail(email) |
2863 | |
2864 | |
2865 | class ValidateTeamEmailView(ValidateEmailView): |
2866 | @@ -595,7 +587,6 @@ |
2867 | # that this new email does not have the PREFERRED status. |
2868 | email.status = EmailAddressStatus.NEW |
2869 | email.personID = requester.id |
2870 | - email.accountID = requester.accountID |
2871 | requester.validateAndEnsurePreferredEmail(email) |
2872 | |
2873 | # Need to flush all changes we made, so subsequent queries we make |
2874 | |
2875 | === modified file 'lib/lp/services/webapp/authentication.py' |
2876 | --- lib/lp/services/webapp/authentication.py 2012-01-01 02:58:52 +0000 |
2877 | +++ lib/lp/services/webapp/authentication.py 2012-01-13 14:12:26 +0000 |
2878 | @@ -68,7 +68,7 @@ |
2879 | if login is not None: |
2880 | login_src = getUtility(IPlacelessLoginSource) |
2881 | principal = login_src.getPrincipalByLogin(login) |
2882 | - if principal is not None and principal.account.is_valid: |
2883 | + if principal is not None and principal.person.is_valid_person: |
2884 | password = credentials.getPassword() |
2885 | if principal.validate(password): |
2886 | # We send a LoggedInEvent here, when the |
2887 | @@ -107,7 +107,7 @@ |
2888 | # available in login source. This happens when account has |
2889 | # become invalid for some reason, such as being merged. |
2890 | return None |
2891 | - elif principal.account.is_valid: |
2892 | + elif principal.person.is_valid_person: |
2893 | login = authdata['login'] |
2894 | assert login, 'login is %s!' % repr(login) |
2895 | notify(CookieAuthPrincipalIdentifiedEvent( |
2896 | @@ -276,13 +276,11 @@ |
2897 | validate the password against so it may then email a validation |
2898 | request to the user and inform them it has done so. |
2899 | """ |
2900 | - try: |
2901 | - account = getUtility(IAccountSet).getByEmail(login) |
2902 | - except LookupError: |
2903 | + person = getUtility(IPersonSet).getByEmail(login) |
2904 | + if person is None or person.account is None: |
2905 | return None |
2906 | - else: |
2907 | - return self._principalForAccount( |
2908 | - account, access_level, scope, want_password) |
2909 | + return self._principalForAccount( |
2910 | + person.account, access_level, scope, want_password) |
2911 | |
2912 | def _principalForAccount(self, account, access_level, scope, |
2913 | want_password=True): |
2914 | |
2915 | === modified file 'lib/lp/services/webapp/login.py' |
2916 | --- lib/lp/services/webapp/login.py 2012-01-01 02:58:52 +0000 |
2917 | +++ lib/lp/services/webapp/login.py 2012-01-13 14:12:26 +0000 |
2918 | @@ -323,11 +323,11 @@ |
2919 | finally: |
2920 | timeline_action.finish() |
2921 | |
2922 | - def login(self, account): |
2923 | + def login(self, person): |
2924 | loginsource = getUtility(IPlacelessLoginSource) |
2925 | # We don't have a logged in principal, so we must remove the security |
2926 | # proxy of the account's preferred email. |
2927 | - email = removeSecurityProxy(account.preferredemail).email |
2928 | + email = removeSecurityProxy(person.preferredemail).email |
2929 | logInPrincipal( |
2930 | self.request, loginsource.getPrincipalByLogin(email), email) |
2931 | |
2932 | @@ -383,7 +383,7 @@ |
2933 | return self.suspended_account_template() |
2934 | |
2935 | with MasterDatabasePolicy(): |
2936 | - self.login(person.account) |
2937 | + self.login(person) |
2938 | |
2939 | if should_update_last_write: |
2940 | # This is a GET request but we changed the database, so update |
2941 | |
2942 | === modified file 'lib/lp/services/webapp/publication.py' |
2943 | --- lib/lp/services/webapp/publication.py 2012-01-01 02:58:52 +0000 |
2944 | +++ lib/lp/services/webapp/publication.py 2012-01-13 14:12:26 +0000 |
2945 | @@ -341,9 +341,10 @@ |
2946 | # automated tests. |
2947 | if request.get('PATH_INFO') not in [u'/+opstats', u'/+haproxy']: |
2948 | principal = auth_utility.authenticate(request) |
2949 | - if principal is None or principal.person is None: |
2950 | - # This is either an unauthenticated user or a user who |
2951 | - # authenticated on our OpenID server using a personless account. |
2952 | + if principal is not None: |
2953 | + assert principal.person is not None |
2954 | + else: |
2955 | + # This is an unauthenticated user. |
2956 | principal = auth_utility.unauthenticatedPrincipal() |
2957 | assert principal is not None, "Missing unauthenticated principal." |
2958 | return principal |
2959 | |
2960 | === modified file 'lib/lp/services/webapp/tests/test_authentication.py' |
2961 | --- lib/lp/services/webapp/tests/test_authentication.py 2012-01-01 02:58:52 +0000 |
2962 | +++ lib/lp/services/webapp/tests/test_authentication.py 2012-01-13 14:12:26 +0000 |
2963 | @@ -9,17 +9,7 @@ |
2964 | import unittest |
2965 | |
2966 | from contrib.oauth import OAuthRequest |
2967 | -from zope.app.security.principalregistry import UnauthenticatedPrincipal |
2968 | - |
2969 | -from lp.services.config import config |
2970 | -from lp.services.webapp.authentication import LaunchpadPrincipal |
2971 | -from lp.services.webapp.login import logInPrincipal |
2972 | -from lp.services.webapp.publication import LaunchpadBrowserPublication |
2973 | -from lp.services.webapp.servers import LaunchpadTestRequest |
2974 | -from lp.testing import ( |
2975 | - login, |
2976 | - TestCaseWithFactory, |
2977 | - ) |
2978 | +from lp.testing import TestCaseWithFactory |
2979 | from lp.testing.layers import ( |
2980 | DatabaseFunctionalLayer, |
2981 | LaunchpadFunctionalLayer, |
2982 | @@ -31,32 +21,6 @@ |
2983 | ) |
2984 | |
2985 | |
2986 | -class TestAuthenticationOfPersonlessAccounts(TestCaseWithFactory): |
2987 | - layer = DatabaseFunctionalLayer |
2988 | - |
2989 | - def setUp(self): |
2990 | - TestCaseWithFactory.setUp(self) |
2991 | - self.email = 'baz@example.com' |
2992 | - self.request = LaunchpadTestRequest() |
2993 | - self.account = self.factory.makeAccount( |
2994 | - 'Personless account', email=self.email) |
2995 | - self.principal = LaunchpadPrincipal( |
2996 | - self.account.id, self.account.displayname, |
2997 | - self.account.displayname, self.account) |
2998 | - login(self.email) |
2999 | - |
3000 | - def test_navigate_anonymously_on_launchpad_dot_net(self): |
3001 | - # A user with the credentials of a personless account will browse |
3002 | - # launchpad.net anonymously. |
3003 | - logInPrincipal(self.request, self.principal, self.email) |
3004 | - self.request.response.setCookie( |
3005 | - config.launchpad_session.cookie, 'xxx') |
3006 | - |
3007 | - publication = LaunchpadBrowserPublication(None) |
3008 | - principal = publication.getPrincipal(self.request) |
3009 | - self.failUnless(isinstance(principal, UnauthenticatedPrincipal)) |
3010 | - |
3011 | - |
3012 | class TestOAuthParsing(TestCaseWithFactory): |
3013 | |
3014 | layer = DatabaseFunctionalLayer |
3015 | |
3016 | === modified file 'lib/lp/services/webapp/tests/test_authutility.py' |
3017 | --- lib/lp/services/webapp/tests/test_authutility.py 2012-01-01 02:58:52 +0000 |
3018 | +++ lib/lp/services/webapp/tests/test_authutility.py 2012-01-13 14:12:26 +0000 |
3019 | @@ -31,16 +31,16 @@ |
3020 | |
3021 | class DummyPerson(object): |
3022 | implements(IPerson) |
3023 | - is_valid = True |
3024 | + is_valid_person = True |
3025 | |
3026 | |
3027 | class DummyAccount(object): |
3028 | implements(IAccount) |
3029 | - is_valid = True |
3030 | person = DummyPerson() |
3031 | |
3032 | |
3033 | Bruce = LaunchpadPrincipal(42, 'bruce', 'Bruce', DummyAccount(), 'bruce!') |
3034 | +Bruce.person = Bruce.account.person |
3035 | |
3036 | |
3037 | class DummyPlacelessLoginSource(object): |
3038 | |
3039 | === modified file 'lib/lp/services/webapp/tests/test_login.py' |
3040 | --- lib/lp/services/webapp/tests/test_login.py 2012-01-04 11:57:57 +0000 |
3041 | +++ lib/lp/services/webapp/tests/test_login.py 2012-01-13 14:12:26 +0000 |
3042 | @@ -292,25 +292,6 @@ |
3043 | self.assertEquals( |
3044 | view.fake_consumer.requested_url, 'http://example.com?foo=bar') |
3045 | |
3046 | - def test_personless_account(self): |
3047 | - # When there is no Person record associated with the account, we |
3048 | - # create one. |
3049 | - account = self.factory.makeAccount('Test account') |
3050 | - self.assertIs(None, IPerson(account, None)) |
3051 | - with SRegResponse_fromSuccessResponse_stubbed(): |
3052 | - view, html = self._createViewWithResponse(account) |
3053 | - self.assertIsNot(None, IPerson(account, None)) |
3054 | - self.assertTrue(view.login_called) |
3055 | - response = view.request.response |
3056 | - self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus()) |
3057 | - self.assertEquals(view.request.form['starting_url'], |
3058 | - response.getHeader('Location')) |
3059 | - |
3060 | - # We also update the last_write flag in the session, to make sure |
3061 | - # further requests use the master DB and thus see the newly created |
3062 | - # stuff. |
3063 | - self.assertLastWriteIsSet(view.request) |
3064 | - |
3065 | def test_unseen_identity(self): |
3066 | # When we get a positive assertion about an identity URL we've never |
3067 | # seen, we automatically register an account with that identity |
3068 | @@ -329,11 +310,11 @@ |
3069 | account = account_set.getByOpenIDIdentifier(identifier) |
3070 | self.assertIsNot(None, account) |
3071 | self.assertEquals(AccountStatus.ACTIVE, account.status) |
3072 | + person = IPerson(account, None) |
3073 | + self.assertIsNot(None, person) |
3074 | + self.assertEquals('Foo User', person.displayname) |
3075 | self.assertEquals('non-existent@example.com', |
3076 | - removeSecurityProxy(account.preferredemail).email) |
3077 | - person = IPerson(account, None) |
3078 | - self.assertIsNot(None, person) |
3079 | - self.assertEquals('Foo User', person.displayname) |
3080 | + removeSecurityProxy(person.preferredemail).email) |
3081 | |
3082 | # We also update the last_write flag in the session, to make sure |
3083 | # further requests use the master DB and thus see the newly created |
3084 | |
3085 | === modified file 'lib/lp/services/webapp/tests/test_login_account.py' |
3086 | --- lib/lp/services/webapp/tests/test_login_account.py 2012-01-01 02:58:52 +0000 |
3087 | +++ lib/lp/services/webapp/tests/test_login_account.py 2012-01-13 14:12:26 +0000 |
3088 | @@ -17,7 +17,6 @@ |
3089 | from lp.services.webapp.authentication import LaunchpadPrincipal |
3090 | from lp.services.webapp.interfaces import ( |
3091 | CookieAuthLoggedInEvent, |
3092 | - ILaunchpadPrincipal, |
3093 | IPlacelessAuthUtility, |
3094 | ) |
3095 | from lp.services.webapp.login import ( |
3096 | @@ -27,7 +26,6 @@ |
3097 | ) |
3098 | from lp.services.webapp.servers import LaunchpadTestRequest |
3099 | from lp.testing import ( |
3100 | - ANONYMOUS, |
3101 | login, |
3102 | TestCaseWithFactory, |
3103 | ) |
3104 | @@ -172,34 +170,3 @@ |
3105 | principal = getUtility(IPlacelessAuthUtility).authenticate( |
3106 | self.request) |
3107 | self.failUnless(principal is None) |
3108 | - |
3109 | - |
3110 | -class TestLoggingInWithPersonlessAccount(TestCaseWithFactory): |
3111 | - layer = DatabaseFunctionalLayer |
3112 | - |
3113 | - def setUp(self): |
3114 | - TestCaseWithFactory.setUp(self) |
3115 | - self.request = LaunchpadTestRequest() |
3116 | - login(ANONYMOUS) |
3117 | - account_set = getUtility(IAccountSet) |
3118 | - account, email = account_set.createAccountAndEmail( |
3119 | - 'foo@example.com', AccountCreationRationale.UNKNOWN, |
3120 | - 'Display name', 'password') |
3121 | - self.principal = LaunchpadPrincipal( |
3122 | - account.id, account.displayname, account.displayname, account) |
3123 | - login('foo@example.com') |
3124 | - |
3125 | - def test_logInPrincipal(self): |
3126 | - # logInPrincipal() will log the given principal in and not worry about |
3127 | - # its lack of an associated Person. |
3128 | - logInPrincipal(self.request, self.principal, 'foo@example.com') |
3129 | - |
3130 | - # Ensure we are using cookie auth. |
3131 | - self.assertIsNotNone( |
3132 | - self.request.response.getCookie(config.launchpad_session.cookie) |
3133 | - ) |
3134 | - |
3135 | - placeless_auth_utility = getUtility(IPlacelessAuthUtility) |
3136 | - principal = placeless_auth_utility.authenticate(self.request) |
3137 | - self.failUnless(ILaunchpadPrincipal.providedBy(principal)) |
3138 | - self.failUnless(principal.person is None) |
3139 | |
3140 | === modified file 'lib/lp/services/webservice/configuration.py' |
3141 | --- lib/lp/services/webservice/configuration.py 2012-01-01 02:58:52 +0000 |
3142 | +++ lib/lp/services/webservice/configuration.py 2012-01-13 14:12:26 +0000 |
3143 | @@ -26,7 +26,7 @@ |
3144 | active_versions = ["beta", "1.0", "devel"] |
3145 | last_version_with_mutator_named_operations = "beta" |
3146 | first_version_with_total_size_link = "devel" |
3147 | - view_permission = "launchpad.View" |
3148 | + view_permission = "launchpad.LimitedView" |
3149 | require_explicit_versions = True |
3150 | compensate_for_mod_compress_etag_modification = True |
3151 | |
3152 | |
3153 | === modified file 'lib/lp/testing/factory.py' |
3154 | --- lib/lp/testing/factory.py 2012-01-04 23:49:46 +0000 |
3155 | +++ lib/lp/testing/factory.py 2012-01-13 14:12:26 +0000 |
3156 | @@ -561,7 +561,7 @@ |
3157 | pocket) |
3158 | return ProxyFactory(location) |
3159 | |
3160 | - def makeAccount(self, displayname=None, email=None, password=None, |
3161 | + def makeAccount(self, displayname=None, password=None, |
3162 | status=AccountStatus.ACTIVE, |
3163 | rationale=AccountCreationRationale.UNKNOWN): |
3164 | """Create and return a new Account.""" |
3165 | @@ -570,13 +570,6 @@ |
3166 | account = getUtility(IAccountSet).new( |
3167 | rationale, displayname, password=password) |
3168 | removeSecurityProxy(account).status = status |
3169 | - if email is None: |
3170 | - email = self.getUniqueEmailAddress() |
3171 | - email_status = EmailAddressStatus.PREFERRED |
3172 | - if status != AccountStatus.ACTIVE: |
3173 | - email_status = EmailAddressStatus.NEW |
3174 | - email = self.makeEmail( |
3175 | - email, person=None, account=account, email_status=email_status) |
3176 | self.makeOpenIdIdentifier(account) |
3177 | return account |
3178 | |
3179 | @@ -731,10 +724,8 @@ |
3180 | # setPreferredEmail no longer activates the account |
3181 | # automatically. |
3182 | account = IMasterStore(Account).get(Account, person.accountID) |
3183 | - account.activate( |
3184 | - "Activated by factory.makePersonByName", |
3185 | - password='foo', |
3186 | - preferred_email=email) |
3187 | + account.reactivate( |
3188 | + "Activated by factory.makePersonByName", password='foo') |
3189 | person.setPreferredEmail(email) |
3190 | |
3191 | if not use_default_autosubscribe_policy: |
3192 | @@ -745,20 +736,16 @@ |
3193 | MailingListAutoSubscribePolicy.NEVER) |
3194 | account = IMasterStore(Account).get(Account, person.accountID) |
3195 | getUtility(IEmailAddressSet).new( |
3196 | - alternative_address, person, EmailAddressStatus.VALIDATED, |
3197 | - account) |
3198 | + alternative_address, person, EmailAddressStatus.VALIDATED) |
3199 | return person |
3200 | |
3201 | - def makeEmail(self, address, person, account=None, email_status=None): |
3202 | + def makeEmail(self, address, person, email_status=None): |
3203 | """Create a new email address for a person. |
3204 | |
3205 | :param address: The email address to create. |
3206 | :type address: string |
3207 | :param person: The person to assign the email address to. |
3208 | :type person: `IPerson` |
3209 | - :param account: The account to assign the email address to. Will use |
3210 | - the given person's account if None is provided. |
3211 | - :type person: `IAccount` |
3212 | :param email_status: The default status of the email address, |
3213 | if given. If not given, `EmailAddressStatus.VALIDATED` |
3214 | will be used. |
3215 | @@ -769,7 +756,7 @@ |
3216 | if email_status is None: |
3217 | email_status = EmailAddressStatus.VALIDATED |
3218 | return getUtility(IEmailAddressSet).new( |
3219 | - address, person, email_status, account) |
3220 | + address, person, email_status) |
3221 | |
3222 | def makeTeam(self, owner=None, displayname=None, email=None, name=None, |
3223 | description=None, icon=None, logo=None, |
3224 | |
3225 | === modified file 'lib/lp/testing/tests/test_login.py' |
3226 | --- lib/lp/testing/tests/test_login.py 2012-01-01 02:58:52 +0000 |
3227 | +++ lib/lp/testing/tests/test_login.py 2012-01-13 14:12:26 +0000 |
3228 | @@ -104,13 +104,6 @@ |
3229 | e = self.assertRaises(ValueError, login_person, team) |
3230 | self.assertEqual(str(e), "Got team, expected person: %r" % (team,)) |
3231 | |
3232 | - def test_login_account(self): |
3233 | - # Calling login_person with an account logs you in with that account. |
3234 | - person = self.factory.makePerson() |
3235 | - account = person.account |
3236 | - login_person(account) |
3237 | - self.assertLoggedIn(person) |
3238 | - |
3239 | def test_login_with_email(self): |
3240 | # login() logs a person in by email. |
3241 | email = 'test-email@example.com' |
3242 | |
3243 | === modified file 'lib/lp/testopenid/browser/server.py' |
3244 | --- lib/lp/testopenid/browser/server.py 2012-01-01 02:58:52 +0000 |
3245 | +++ lib/lp/testopenid/browser/server.py 2012-01-13 14:12:26 +0000 |
3246 | @@ -230,9 +230,10 @@ |
3247 | else: |
3248 | response = self.openid_request.answer(True) |
3249 | |
3250 | + person = IPerson(self.account) |
3251 | sreg_fields = dict( |
3252 | - nickname=IPerson(self.account).name, |
3253 | - email=self.account.preferredemail.email, |
3254 | + nickname=person.name, |
3255 | + email=person.preferredemail.email, |
3256 | fullname=self.account.displayname) |
3257 | sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request) |
3258 | sreg_response = SRegResponse.extractResponse( |
3259 | |
3260 | === modified file 'lib/lp/translations/utilities/tests/test_file_importer.py' |
3261 | --- lib/lp/translations/utilities/tests/test_file_importer.py 2011-12-30 01:48:17 +0000 |
3262 | +++ lib/lp/translations/utilities/tests/test_file_importer.py 2012-01-13 14:12:26 +0000 |
3263 | @@ -353,21 +353,6 @@ |
3264 | po_importer.potemplate.displayname), |
3265 | 'Did not create the correct comment for %s' % test_email) |
3266 | |
3267 | - def test_getPersonByEmail_personless_account(self): |
3268 | - # An Account without a Person attached is a difficult case for |
3269 | - # _getPersonByEmail: it has to create the Person but re-use an |
3270 | - # existing Account and email address. |
3271 | - (pot_importer, po_importer) = self._createImporterForExportedEntries() |
3272 | - test_email = 'freecdsplease@example.com' |
3273 | - account = self.factory.makeAccount('Send me Ubuntu', test_email) |
3274 | - |
3275 | - person = po_importer._getPersonByEmail(test_email) |
3276 | - |
3277 | - self.assertEqual(account, person.account) |
3278 | - |
3279 | - # The same person will come up for the same address next time. |
3280 | - self.assertEqual(person, po_importer._getPersonByEmail(test_email)) |
3281 | - |
3282 | def test_getPersonByEmail_bad_address(self): |
3283 | # _getPersonByEmail returns None for malformed addresses. |
3284 | (pot_importer, po_importer) = self._createImporterForExportedEntries() |
Looks good.
I'm told we want to order specifications by last update time, so we will need an index on that column.