Merge lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications into lp:launchpad/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
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
Stuart Bishop (stub) wrote :

Looks good.

I'm told we want to order specifications by last update time, so we will need an index on that column.

review: Approve (db)
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 ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
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 ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
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 ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
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>&#160;
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>&#160;
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()

Subscribers

People subscribed via source and target branches

to status/vote changes: