launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06085
lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications into lp:launchpad/db-devel
Ursula Junque has proposed merging lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications into lp:launchpad/db-devel.
Requested reviews:
Robert Collins (lifeless): db
Stuart Bishop (stub): db
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications/+merge/88503
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.
--
https://code.launchpad.net/~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications/+merge/88503
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~ursinha/launchpad/add-date-last-changed-who-changed-to-specifications into lp:launchpad/db-devel.
=== modified file 'buildout.cfg'
--- buildout.cfg 2011-12-30 06:47:17 +0000
+++ buildout.cfg 2012-01-13 14:02:28 +0000
@@ -43,7 +43,7 @@
rm -rf ${buildout:yui-directory}/yui-${versions:yui}/*
tar -zxf download-cache/dist/yui-${versions:yui}.tar.gz \
-C ${buildout:yui-directory}/yui-${versions:yui}
- ln -s ../../../../${buildout:yui-directory}/yui-${versions:yui} \
+ ln -sf ../../../../${buildout:yui-directory}/yui-${versions:yui} \
lib/canonical/launchpad/icing/yui
[filetemplates]
=== added file 'database/schema/patch-2209-04-0.sql'
--- database/schema/patch-2209-04-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2209-04-0.sql 2012-01-13 14:02:28 +0000
@@ -0,0 +1,16 @@
+-- Copyright 2011 Canonical Ltd. This software is licensed under the
+-- GNU Affero General Public License version 3 (see the file LICENSE).
+
+SET client_min_messages=ERROR;
+
+ALTER TABLE specification
+ ADD COLUMN date_last_changed timestamp without time zone,
+ ADD COLUMN last_changed_by integer REFERENCES person;
+
+ALTER TABLE specification ALTER COLUMN date_last_changed SET DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC');
+
+CREATE INDEX specification__last_changed_by__idx ON specification USING btree (last_changed_by) WHERE (last_changed_by IS NOT NULL);
+
+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 04, 0);
+
+
=== modified file 'lib/canonical/launchpad/icing/css/base.css'
--- lib/canonical/launchpad/icing/css/base.css 2011-12-07 04:57:36 +0000
+++ lib/canonical/launchpad/icing/css/base.css 2012-01-13 14:02:28 +0000
@@ -264,6 +264,9 @@
table.sortable img.sortarrow {
padding-left: 2px;
}
+table.sortable th {
+ cursor: pointer;
+ }
th.ascending {
background-image: url(/@@/arrowDown);
background-position: center right;
=== modified file 'lib/canonical/launchpad/icing/css/components/bug_listing.css'
--- lib/canonical/launchpad/icing/css/components/bug_listing.css 2011-12-21 06:19:51 +0000
+++ lib/canonical/launchpad/icing/css/components/bug_listing.css 2012-01-13 14:02:28 +0000
@@ -15,13 +15,10 @@
float: left;
padding-right: 10px;
}
-div.buglisting-col2,
-div.buglisting-col3 {
+div.buglisting-col2 {
float: left;
margin-top: 2px;
- }
-div.buglisting-col2 {
- width: 40%;
+ width: 68%;
}
div#client-listing .status,
div#client-listing .importance {
@@ -33,7 +30,7 @@
line-height: 12px;
text-align: center;
margin: 5px 10px 0 0;
-}
+ }
div#client-listing .importance {
width: 65px;
}
=== modified file 'lib/lp/app/javascript/comment.js'
--- lib/lp/app/javascript/comment.js 2011-08-09 14:18:02 +0000
+++ lib/lp/app/javascript/comment.js 2012-01-13 14:02:28 +0000
@@ -20,11 +20,13 @@
*/
initializer: function() {
this.submit_button = this.get_submit();
- this.comment_input = Y.one('[id="field.comment"]');
+ this.comment_input = Y.one(
+ 'div#add-comment-form [id="field.comment"]');
this.lp_client = new Y.lp.client.Launchpad();
this.error_handler = new Y.lp.client.ErrorHandler();
- this.error_handler.clearProgressUI = bind(this.clearProgressUI, this);
- this.error_handler.showError = bind(function (error_msg) {
+ this.error_handler.clearProgressUI = Y.bind(
+ this.clearProgressUI, this);
+ this.error_handler.showError = Y.bind(function (error_msg) {
Y.lp.app.errors.display_error(this.submit_button, error_msg);
}, this);
this.progress_message = Y.Node.create(
@@ -39,7 +41,7 @@
* @method get_submit
*/
get_submit: function(){
- return Y.one('[id="field.actions.save"]');
+ return Y.one('div#add-comment-form input[id="field.actions.save"]');
},
/**
* Implementation of Widget.renderUI.
@@ -86,9 +88,9 @@
return;
}
this.activateProgressUI('Saving...');
- this.post_comment(bind(function(message_entry) {
+ this.post_comment(Y.bind(function(message_entry) {
this.get_comment_HTML(
- message_entry, bind(this.insert_comment_HTML, this));
+ message_entry, Y.bind(this.insert_comment_HTML, this));
this._add_comment_success();
}, this));
},
@@ -186,9 +188,9 @@
* @method bindUI
*/
bindUI: function(){
- this.comment_input.on('keyup', bind(this.syncUI, this));
- this.comment_input.on('mouseup', bind(this.syncUI, this));
- this.submit_button.on('click', bind(this.add_comment, this));
+ this.comment_input.on('keyup', this.syncUI, this);
+ this.comment_input.on('mouseup', this.syncUI, this);
+ this.submit_button.on('click', this.add_comment, this);
},
/**
* Implementation of Widget.syncUI: Update appearance according to state.
@@ -325,7 +327,7 @@
window.scrollTo(0, Y.one('#add-comment').getY());
this.lp_client.get(object_url, {
on: {
- success: bind(function(comment){
+ success: Y.bind(function(comment){
this.set_in_reply_to(comment);
this.clearProgressUI();
this.syncUI();
@@ -380,11 +382,11 @@
*/
bindUI: function() {
CodeReviewComment.superclass.bindUI.apply(this);
- this.vote_input.on('keyup', bind(this.syncUI, this));
- this.vote_input.on('change', bind(this.syncUI, this));
- this.review_type.on('keyup', bind(this.syncUI, this));
- this.review_type.on('mouseup', bind(this.syncUI, this));
- Y.all('a.menu-link-reply').on('click', bind(this.reply_clicked, this));
+ this.vote_input.on('keyup', this.syncUI, this);
+ this.vote_input.on('change', this.syncUI, this);
+ this.review_type.on('keyup', this.syncUI, this);
+ this.review_type.on('mouseup', this.syncUI, this);
+ Y.all('a.menu-link-reply').on('click', this.reply_clicked, this);
},
/**
* Implementation of Widget.syncUI: Update appearance according to state.
=== modified file 'lib/lp/app/javascript/extras/extras.js'
--- lib/lp/app/javascript/extras/extras.js 2011-08-05 09:23:53 +0000
+++ lib/lp/app/javascript/extras/extras.js 2012-01-13 14:02:28 +0000
@@ -10,8 +10,6 @@
YUI.add('lp.extras', function(Y) {
-Y.log('loading lp.extras');
-
var namespace = Y.namespace("lp.extras"),
NodeList = Y.NodeList;
=== modified file 'lib/lp/app/javascript/foldables.js'
--- lib/lp/app/javascript/foldables.js 2012-01-10 17:23:23 +0000
+++ lib/lp/app/javascript/foldables.js 2012-01-13 14:02:28 +0000
@@ -46,7 +46,6 @@
included.each(function (span, index, list) {
if (span.hasClass('foldable-quoted')) {
var quoted_lines = span.all('br');
- debugger;
if (quoted_lines && quoted_lines.size() <= 11) {
// We do not hide short quoted passages (12 lines) by
// default.
=== modified file 'lib/lp/app/javascript/sorttable/sorttable.js'
--- lib/lp/app/javascript/sorttable/sorttable.js 2009-06-03 14:52:54 +0000
+++ lib/lp/app/javascript/sorttable/sorttable.js 2012-01-13 14:02:28 +0000
@@ -1,309 +1,469 @@
-/*
- * Table sorting Javascript. MIT-licensed. Originally from
- *
- * http://www.kryogenix.org/code/browser/sorttable/
- *
- * 2005-07-13: Initial import. Changed the arrow code to use the images
- * included with Plone instead of the entities. Also changed
- * the way sorting of a previously-sorted column behaves
- * slightly. Reformatted to try keeping to 80 columns. (kiko)
- *
- * 2006-03-13: Added support for indicating sortkeys (direct and
- * reverse) inside table cells. Removed tabs. (kiko)
- *
- * 2006-04-08: Fixed matching of "sortable". Added support for "initial-sort".
- * Made sorting stable. (ddaa)
- *
- * 2006-04-11: Fixed numeric sorting to be robust in the presence of
- * whitespace; added trim(). Note that parseFloat() deals
- * with leading and trailing whitespace just fine. (kiko)
- *
- * 2006-04-12: Added default-sort and default-revsort classes. When
- * the data in the table is already pre-sorted, you can use
- * these classes to indicate by which column they were
- * ordered by. This, in turn, allows us to correctly
- * Javascript-sort them later. (kiko)
- *
- * 2006-10-15: Moved the window load event to main-template.pt, so that this
- * script's load event doesn't stomp those of other scripts. (mpt)
+/*
+ SortTable
+ version 2
+ 7th April 2007
+ Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
+
+ Instructions:
+ Download this file
+ Add <script src="sorttable.js"></script> to your HTML
+ Add class="sortable" to any table you'd like to make sortable
+ Click on the headers to sort
+
+ Thanks to many, many people for contributions and suggestions.
+ Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+ This basically means: do what you want with it.
+*/
+
+/**
+ * New api is:
+ * var sortable = new Y.lp.app.sortable.Sortable();
+ * sortable.init();
*/
-var SORT_COLUMN_INDEX;
-
-var arrowUp = "/@@/arrowUp";
-var arrowDown = "/@@/arrowDown";
-var arrowBlank = "/@@/arrowBlank";
-
-function trim(str) {
- return str.replace(/^\s*|\s*$/g, "");
-}
-
-function sortables_init() {
- // Find all tables with class sortable and make them sortable
- if (!document.getElementsByTagName) return;
- tbls = document.getElementsByTagName("table");
- for (ti=0;ti<tbls.length;ti++) {
- thisTbl = tbls[ti];
- if (((' '+thisTbl.className+' ').indexOf(" sortable ") != -1) &&
- (thisTbl.id)) {
- ts_makeSortable(thisTbl);
- }
- }
-}
-
-function ts_makeSortable(table) {
- if (table.tHead && table.tHead.rows && table.tHead.rows.length > 0) {
- var firstRow = table.tHead.rows[0];
- } else if (table.rows && table.rows.length > 0) {
- var firstRow = table.rows[0];
- }
- if (!firstRow) return;
-
- // We have a first row: assume it's the header, and make its
- // contents clickable links
- for (var i=0;i<firstRow.cells.length;i++) {
- var cell = firstRow.cells[i];
- var txt = ts_getInnerText(cell);
- cell.innerHTML = '<a href="#" class="sortheader" onclick="ts_resortTable(this); return false;">'
- + txt +
- '<img class="sortarrow" src="'+arrowBlank+'" height="6" width="9"></a>';
- }
-
- // Sort by the first column whose title cell has initial-sort class.
- for (var i=0; i<firstRow.cells.length; i++) {
- var cell = firstRow.cells[i];
- var lnk = ts_firstChildByName(cell, 'A');
- var img = ts_firstChildByName(lnk, 'IMG')
- if ((' ' + cell.className + ' ').indexOf(" default-sort ") != -1) {
- ts_arrowDown(img);
- }
- if ((' ' + cell.className + ' ').indexOf(" default-revsort ") != -1) {
- ts_arrowUp(img);
- }
- if ((' ' + cell.className + ' ').indexOf(" initial-sort ") != -1) {
- ts_resortTable(lnk);
- }
- }
-}
-
-function ts_getInnerText(el) {
- if (typeof el == "string") return el;
- if (typeof el == "undefined") { return el };
- if (el.innerText) return el.innerText; //Not needed but it is faster
- var str = "";
-
- var cs = el.childNodes;
- var l = cs.length;
- for (var i = 0; i < l; i++) {
- node = cs[i];
- switch (node.nodeType) {
- case 1: //ELEMENT_NODE
- if (node.className == "sortkey") {
- return ts_getInnerText(node);
- } else if (node.className == "revsortkey") {
- return "-" + ts_getInnerText(node);
- } else {
- str += ts_getInnerText(node);
- break;
- }
- case 3: //TEXT_NODE
- str += node.nodeValue;
- break;
- }
- }
- return str;
-}
-
-function ts_firstChildByName(el, name) {
- for (var ci=0; ci < el.childNodes.length; ci++) {
- if (el.childNodes[ci].tagName &&
- el.childNodes[ci].tagName.toLowerCase() == name.toLowerCase())
- return el.childNodes[ci];
- }
-}
-
-function ts_arrowUp(img) {
- img.setAttribute('sortdir','up');
- img.src = arrowUp;
-}
-
-function ts_arrowDown(img) {
- img.setAttribute('sortdir','down');
- img.src = arrowDown;
-}
-
-function ts_resortTable(lnk) {
- // get the img
- var img = ts_firstChildByName(lnk, 'IMG')
- var td = lnk.parentNode;
- var column = td.cellIndex;
- var table = getParent(td,'TABLE');
-
- if (table.rows.length <= 1) return;
-
- SORT_COLUMN_INDEX = column;
- // If some previous column contains a colspan, we need to increase
- // the column index to compensate for it.
- while (td.previousSibling != null) {
- td = td.previousSibling;
- if (td.nodeType != 1) {
- continue
- }
- colspan = td.getAttribute("colspan");
- if (colspan) {
- SORT_COLUMN_INDEX += parseInt(colspan) - 1;
- }
- }
-
- // Work out a type for the column
- var itm = ts_getInnerText(table.rows[1].cells[SORT_COLUMN_INDEX]);
- itm = trim(itm);
-
- sortfn = ts_sort_caseinsensitive;
- if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
- if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date;
- if (itm.match(/^[�$]/)) sortfn = ts_sort_currency;
- if (itm.match(/^-?[\d\.]+$/)) sortfn = ts_sort_numeric;
-
- var firstRow = new Array();
- var newRows = new Array();
- for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; }
- for (j=1;j<table.rows.length;j++) {
- newRows[j-1] = table.rows[j];
- newRows[j-1].oldPosition = j-1;
- }
-
- newRows.sort(ts_stableSort(sortfn));
-
- if (img.getAttribute("sortdir") == 'down') {
- newRows.reverse();
- ts_arrowUp(img);
- } else {
- ts_arrowDown(img);
- }
-
- // We appendChild rows that already exist to the tbody, so it moves
- // them rather than creating new ones
- for (i=0;i<newRows.length;i++) {
- if (!newRows[i].className ||
- (newRows[i].className &&
- (newRows[i].className.indexOf('sortbottom') == -1)))
- // don't do sortbottom rows
- table.tBodies[0].appendChild(newRows[i]);
- }
- // do sortbottom rows only
- for (i=0;i<newRows.length;i++) {
- if (newRows[i].className &&
- (newRows[i].className.indexOf('sortbottom') != -1))
- table.tBodies[0].appendChild(newRows[i]);
- }
-
- // Delete any other arrows there may be showing
- var allimgs = document.getElementsByTagName("img");
- for (var ci=0; ci<allimgs.length; ci++) {
- var one_img = allimgs[ci];
- if (one_img != img &&
- one_img.className == 'sortarrow' &&
- getParent(one_img, "table") == getParent(lnk, "table")) {
- one_img.src = arrowBlank;
- one_img.setAttribute('sortdir', '');
- }
- }
-}
-
-function getParent(el, pTagName) {
- if (el == null)
- return null;
- else if (el.nodeType == 1 &&
- el.tagName.toLowerCase() == pTagName.toLowerCase())
- // Gecko bug, supposed to be uppercase
- return el;
- else
- return getParent(el.parentNode, pTagName);
-}
-
-function ts_stableSort(sortfn) {
- // Return a comparison function based on sortfn, but using oldPosition
- // attributes to discriminate between objects that sortfn compares as
- // equal, effectively providing stable sort.
- function stableSort(a, b) {
- var cmp = sortfn(a, b);
- if (cmp != 0) {
- return cmp;
- } else {
- return a.oldPosition - b.oldPosition;
- }
- }
- return stableSort;
-}
-
-function ts_sort_date(a,b) {
- // y2k notes: two digit years less than 50 are treated as 20XX,
- // greater than 50 are treated as 19XX
- aa = trim(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
- bb = trim(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
- if (aa.length == 10) {
- dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
- } else {
- yr = aa.substr(6,2);
- if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
- dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
- }
- if (bb.length == 10) {
- dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
- } else {
- yr = bb.substr(6,2);
- if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
- dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
- }
- if (dt1==dt2) return 0;
- if (dt1<dt2) return -1;
- return 1;
-}
-
-function ts_sort_currency(a,b) {
- aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
- bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
- return parseFloat(aa) - parseFloat(bb);
-}
-
-function ts_sort_numeric(a,b) {
- aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
- if (isNaN(aa)) aa = 0;
- bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
- if (isNaN(bb)) bb = 0;
- return aa-bb;
-}
-
-function ts_sort_caseinsensitive(a,b) {
- aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
- bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
- if (aa==bb) return 0;
- if (aa<bb) return -1;
- return 1;
-}
-
-function ts_sort_default(a,b) {
- aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
- bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
- if (aa==bb) return 0;
- if (aa<bb) return -1;
- return 1;
-}
-
-
-// addEvent and removeEvent
-// cross-browser event handling for IE5+, NS6 and Mozilla
-// By Scott Andrew
-function addEvent(elm, evType, fn, useCapture) {
- if (elm.addEventListener){
- elm.addEventListener(evType, fn, useCapture);
- return true;
- } else if (elm.attachEvent){
- var r = elm.attachEvent("on"+evType, fn);
- return r;
- } else {
- alert("Handler could not be removed");
- }
-}
-
+
+YUI.add('lp.app.sorttable', function(Y) {
+
+ var namespace = Y.namespace('lp.app.sorttable');
+
+ var stIsIE = /*@cc_on!@*/false;
+
+ sorttable = {
+ init: function() {
+ // quit if this function has already been called
+ if (arguments.callee.done) return;
+ // flag this function so we don't do the same thing twice
+ arguments.callee.done = true;
+
+ if (!document.createElement || !document.getElementsByTagName) return;
+
+ sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
+
+ forEach(document.getElementsByTagName('table'), function(table) {
+ if (table.className.search(/\bsortable\b/) != -1) {
+ sorttable.makeSortable(table);
+ }
+ });
+
+ },
+
+ makeSortable: function(table) {
+ if (table.getElementsByTagName('thead').length == 0) {
+ // table doesn't have a tHead. Since it should have, create one and
+ // put the first table row in it.
+ the = document.createElement('thead');
+ the.appendChild(table.rows[0]);
+ table.insertBefore(the,table.firstChild);
+ }
+ // Safari doesn't support table.tHead, sigh
+ if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
+
+ if (table.tHead.rows.length != 1) return; // can't cope with two header rows
+
+ // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
+ // "total" rows, for example). This is B&R, since what you're supposed
+ // to do is put them in a tfoot. So, if there are sortbottom rows,
+ // for backwards compatibility, move them to tfoot (creating it if needed).
+ sortbottomrows = [];
+ for (var i=0; i<table.rows.length; i++) {
+ if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
+ sortbottomrows[sortbottomrows.length] = table.rows[i];
+ }
+ }
+ if (sortbottomrows) {
+ if (table.tFoot == null) {
+ // table doesn't have a tfoot. Create one.
+ tfo = document.createElement('tfoot');
+ table.appendChild(tfo);
+ }
+ for (var i=0; i<sortbottomrows.length; i++) {
+ tfo.appendChild(sortbottomrows[i]);
+ }
+ delete sortbottomrows;
+ }
+
+ // work through each column and calculate its type
+ headrow = table.tHead.rows[0].cells;
+ for (var i=0; i<headrow.length; i++) {
+ // manually override the type with a sorttable_type attribute
+ if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
+ mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
+ if (mtch) { override = mtch[1]; }
+ if (mtch && typeof sorttable["sort_"+override] == 'function') {
+ headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
+ } else {
+ headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
+ }
+ // make it clickable to sort
+ headrow[i].sorttable_columnindex = i;
+ headrow[i].sorttable_tbody = table.tBodies[0];
+ dean_addEvent(headrow[i],"click", function(e) {
+
+ if (this.className.search(/\bsorttable_sorted\b/) != -1) {
+ // if we're already sorted by this column, just
+ // reverse the table, which is quicker
+ sorttable.reverse(this.sorttable_tbody);
+ this.className = this.className.replace('sorttable_sorted',
+ 'sorttable_sorted_reverse');
+ this.removeChild(document.getElementById('sorttable_sortfwdind'));
+ sortrevind = document.createElement('span');
+ sortrevind.id = "sorttable_sortrevind";
+ sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴';
+ this.appendChild(sortrevind);
+ return;
+ }
+ if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
+ // if we're already sorted by this column in reverse, just
+ // re-reverse the table, which is quicker
+ sorttable.reverse(this.sorttable_tbody);
+ this.className = this.className.replace('sorttable_sorted_reverse',
+ 'sorttable_sorted');
+ this.removeChild(document.getElementById('sorttable_sortrevind'));
+ sortfwdind = document.createElement('span');
+ sortfwdind.id = "sorttable_sortfwdind";
+ sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
+ this.appendChild(sortfwdind);
+ return;
+ }
+
+ // remove sorttable_sorted classes
+ theadrow = this.parentNode;
+ forEach(theadrow.childNodes, function(cell) {
+ if (cell.nodeType == 1) { // an element
+ cell.className = cell.className.replace('sorttable_sorted_reverse','');
+ cell.className = cell.className.replace('sorttable_sorted','');
+ }
+ });
+ sortfwdind = document.getElementById('sorttable_sortfwdind');
+ if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
+ sortrevind = document.getElementById('sorttable_sortrevind');
+ if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
+
+ this.className += ' sorttable_sorted';
+ sortfwdind = document.createElement('span');
+ sortfwdind.id = "sorttable_sortfwdind";
+ sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
+ this.appendChild(sortfwdind);
+
+ // build an array to sort. This is a Schwartzian transform thing,
+ // i.e., we "decorate" each row with the actual sort key,
+ // sort based on the sort keys, and then put the rows back in order
+ // which is a lot faster because you only do getInnerText once per row
+ row_array = [];
+ col = this.sorttable_columnindex;
+ rows = this.sorttable_tbody.rows;
+ for (var j=0; j<rows.length; j++) {
+ row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
+ }
+ /* If you want a stable sort, uncomment the following line */
+ //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
+ /* and comment out this one */
+ row_array.sort(this.sorttable_sortfunction);
+
+ tb = this.sorttable_tbody;
+ for (var j=0; j<row_array.length; j++) {
+ tb.appendChild(row_array[j][1]);
+ }
+
+ delete row_array;
+ });
+ }
+ }
+ },
+
+ guessType: function(table, column) {
+ // guess the type of a column based on its first non-blank row
+ sortfn = sorttable.sort_alpha;
+ for (var i=0; i<table.tBodies[0].rows.length; i++) {
+ text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
+ if (text != '') {
+ if (text.match(/^-?[�$�]?[\d,.]+%?$/)) {
+ return sorttable.sort_numeric;
+ }
+ // check for a date: dd/mm/yyyy or dd/mm/yy
+ // can have / or . or - as separator
+ // can be mm/dd as well
+ possdate = text.match(sorttable.DATE_RE)
+ if (possdate) {
+ // looks like a date
+ first = parseInt(possdate[1]);
+ second = parseInt(possdate[2]);
+ if (first > 12) {
+ // definitely dd/mm
+ return sorttable.sort_ddmm;
+ } else if (second > 12) {
+ return sorttable.sort_mmdd;
+ } else {
+ // looks like a date, but we can't tell which, so assume
+ // that it's dd/mm (English imperialism!) and keep looking
+ sortfn = sorttable.sort_ddmm;
+ }
+ }
+ }
+ }
+ return sortfn;
+ },
+
+ getInnerText: function(node) {
+ // gets the text we want to use for sorting for a cell.
+ // strips leading and trailing whitespace.
+ // this is *not* a generic getInnerText function; it's special to sorttable.
+ // for example, you can override the cell text with a customkey attribute.
+ // it also gets .value for <input> fields.
+
+ hasInputs = (typeof node.getElementsByTagName == 'function') &&
+ node.getElementsByTagName('input').length;
+
+ if (node.getAttribute("sorttable_customkey") != null) {
+ return node.getAttribute("sorttable_customkey");
+ }
+ else if (typeof node.textContent != 'undefined' && !hasInputs) {
+ return node.textContent.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.innerText != 'undefined' && !hasInputs) {
+ return node.innerText.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.text != 'undefined' && !hasInputs) {
+ return node.text.replace(/^\s+|\s+$/g, '');
+ }
+ else {
+ switch (node.nodeType) {
+ case 3:
+ if (node.nodeName.toLowerCase() == 'input') {
+ return node.value.replace(/^\s+|\s+$/g, '');
+ }
+ case 4:
+ return node.nodeValue.replace(/^\s+|\s+$/g, '');
+ break;
+ case 1:
+ case 11:
+ var innerText = '';
+ for (var i = 0; i < node.childNodes.length; i++) {
+ innerText += sorttable.getInnerText(node.childNodes[i]);
+ }
+ return innerText.replace(/^\s+|\s+$/g, '');
+ break;
+ default:
+ return '';
+ }
+ }
+ },
+
+ reverse: function(tbody) {
+ // reverse the rows in a tbody
+ newrows = [];
+ for (var i=0; i<tbody.rows.length; i++) {
+ newrows[newrows.length] = tbody.rows[i];
+ }
+ for (var i=newrows.length-1; i>=0; i--) {
+ tbody.appendChild(newrows[i]);
+ }
+ delete newrows;
+ },
+
+ /* sort functions
+ each sort function takes two parameters, a and b
+ you are comparing a[0] and b[0] */
+ sort_numeric: function(a,b) {
+ aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
+ if (isNaN(aa)) aa = 0;
+ bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
+ if (isNaN(bb)) bb = 0;
+ return aa-bb;
+ },
+ sort_alpha: function(a,b) {
+ if (a[0]==b[0]) return 0;
+ if (a[0]<b[0]) return -1;
+ return 1;
+ },
+ sort_ddmm: function(a,b) {
+ mtch = a[0].match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b[0].match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+ sort_mmdd: function(a,b) {
+ mtch = a[0].match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b[0].match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ shaker_sort: function(list, comp_func) {
+ // A stable sort function to allow multi-level sorting of data
+ // see: http://en.wikipedia.org/wiki/Cocktail_sort
+ // thanks to Joseph Nahmias
+ var b = 0;
+ var t = list.length - 1;
+ var swap = true;
+
+ while(swap) {
+ swap = false;
+ for(var i = b; i < t; ++i) {
+ if ( comp_func(list[i], list[i+1]) > 0 ) {
+ var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
+ swap = true;
+ }
+ } // for
+ t--;
+
+ if (!swap) break;
+
+ for(var i = t; i > b; --i) {
+ if ( comp_func(list[i], list[i-1]) < 0 ) {
+ var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
+ swap = true;
+ }
+ } // for
+ b++;
+
+ } // while(swap)
+ }
+ }
+
+ // written by Dean Edwards, 2005
+ // with input from Tino Zijdel, Matthias Miller, Diego Perini
+
+ // http://dean.edwards.name/weblog/2005/10/add-event/
+
+ function dean_addEvent(element, type, handler) {
+ if (element.addEventListener) {
+ element.addEventListener(type, handler, false);
+ } else {
+ // assign each event handler a unique ID
+ if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
+ // create a hash table of event types for the element
+ if (!element.events) element.events = {};
+ // create a hash table of event handlers for each element/event pair
+ var handlers = element.events[type];
+ if (!handlers) {
+ handlers = element.events[type] = {};
+ // store the existing event handler (if there is one)
+ if (element["on" + type]) {
+ handlers[0] = element["on" + type];
+ }
+ }
+ // store the event handler in the hash table
+ handlers[handler.$$guid] = handler;
+ // assign a global event handler to do all the work
+ element["on" + type] = handleEvent;
+ }
+ };
+ // a counter used to create unique IDs
+ dean_addEvent.guid = 1;
+
+ function removeEvent(element, type, handler) {
+ if (element.removeEventListener) {
+ element.removeEventListener(type, handler, false);
+ } else {
+ // delete the event handler from the hash table
+ if (element.events && element.events[type]) {
+ delete element.events[type][handler.$$guid];
+ }
+ }
+ };
+
+ function handleEvent(event) {
+ var returnValue = true;
+ // grab the event object (IE uses a global event object)
+ event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+ // get a reference to the hash table of event handlers
+ var handlers = this.events[event.type];
+ // execute each event handler
+ for (var i in handlers) {
+ this.$$handleEvent = handlers[i];
+ if (this.$$handleEvent(event) === false) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+ };
+
+ function fixEvent(event) {
+ // add W3C standard event methods
+ event.preventDefault = fixEvent.preventDefault;
+ event.stopPropagation = fixEvent.stopPropagation;
+ return event;
+ };
+ fixEvent.preventDefault = function() {
+ this.returnValue = false;
+ };
+ fixEvent.stopPropagation = function() {
+ this.cancelBubble = true;
+ }
+
+ // Dean's forEach: http://dean.edwards.name/base/forEach.js
+ /*
+ forEach, version 1.0
+ Copyright 2006, Dean Edwards
+ License: http://www.opensource.org/licenses/mit-license.php
+ */
+
+ // array-like enumeration
+ if (!Array.forEach) { // mozilla already supports this
+ Array.forEach = function(array, block, context) {
+ for (var i = 0; i < array.length; i++) {
+ block.call(context, array[i], i, array);
+ }
+ };
+ }
+
+ // generic enumeration
+ Function.prototype.forEach = function(object, block, context) {
+ for (var key in object) {
+ if (typeof this.prototype[key] == "undefined") {
+ block.call(context, object[key], key, object);
+ }
+ }
+ };
+
+ // character enumeration
+ String.forEach = function(string, block, context) {
+ Array.forEach(string.split(""), function(chr, index) {
+ block.call(context, chr, index, string);
+ });
+ };
+
+ // globally resolve forEach enumeration
+ var forEach = function(object, block, context) {
+ if (object) {
+ var resolve = Object; // default
+ if (object instanceof Function) {
+ // functions have a "length" property
+ resolve = Function;
+ } else if (object.forEach instanceof Function) {
+ // the object implements a custom forEach method so use that
+ object.forEach(block, context);
+ return;
+ } else if (typeof object == "string") {
+ // the object is a string
+ resolve = String;
+ } else if (typeof object.length == "number") {
+ // the object is array-like
+ resolve = Array;
+ }
+ resolve.forEach(object, block, context);
+ }
+ };
+
+ namespace.SortTable = sorttable;
+
+}, "0.1", {});
=== modified file 'lib/lp/app/javascript/tests/test_foldables.js'
--- lib/lp/app/javascript/tests/test_foldables.js 2012-01-10 17:36:29 +0000
+++ lib/lp/app/javascript/tests/test_foldables.js 2012-01-13 14:02:28 +0000
@@ -82,7 +82,6 @@
},
test_doesnt_hide_short: function () {
- debugger;
this._add_comment(quote_comment);
Y.lp.app.foldables.activate();
Y.Assert.isNull(Y.one('a'));
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2012-01-11 13:59:39 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2012-01-13 14:02:28 +0000
@@ -106,10 +106,11 @@
</script>
<script id="base-layout-load-scripts" type="text/javascript">
- LPS.use('node', 'event-delegate', 'lp', 'lp.app.foldables', 'lp.app.links',
- 'lp.app.longpoll', 'lp.app.inlinehelp', function(Y) {
+ LPS.use('node', 'event-delegate', 'lp', 'lp.app.foldables',
+ 'lp.app.links', 'lp.app.sorttable', 'lp.app.longpoll',
+ 'lp.app.inlinehelp', function(Y) {
Y.on('load', function(e) {
- sortables_init();
+ Y.lp.app.sorttable.SortTable.init();
Y.lp.app.inlinehelp.init_help();
Y.lp.activate_collapsibles();
Y.lp.app.foldables.activate();
=== modified file 'lib/lp/archiveuploader/tests/nascentuploadfile.txt'
--- lib/lp/archiveuploader/tests/nascentuploadfile.txt 2011-12-30 06:14:56 +0000
+++ lib/lp/archiveuploader/tests/nascentuploadfile.txt 2012-01-13 14:02:28 +0000
@@ -270,37 +270,6 @@
>>> addr['person'].creation_comment
u'when the some-source_6.6.6 package was uploaded to hoary/RELEASE'
-If the email address is registered but not associated with a person it will be
-associated with a new Person. This involves updating the email address,
-something for which the uploader must have explicit permissions (bug 589073).
-
- >>> sig_file.policy.create_people
- True
-
- >>> from lp.services.database.sqlbase import commit
- >>> from lp.services.identity.interfaces.account import IAccountSet
- >>> from lp.registry.interfaces.person import (
- ... PersonCreationRationale, IPersonSet)
- >>> (acct, email) = getUtility(IAccountSet).createAccountAndEmail(
- ... "foo@xxxxxxxxxxxxx", PersonCreationRationale.UNKNOWN,
- ... "fo", "secr1t")
- >>> person = getUtility(IPersonSet).createPersonWithoutEmail("fo",
- ... rationale=PersonCreationRationale.UNKNOWN)
- >>> person.account = acct
-
- Commit the changes so the emailaddress will have to be updated later
- rather than inserted.
-
- >>> commit()
-
- >>> addr = sig_file.parseAddress("Foo <foo@xxxxxxxxxxxxx>")
- >>> print addr['person'].creation_rationale.name
- UNKNOWN
- >>> commit()
-
- >>> print addr['email']
- foo@xxxxxxxxxxxxx
-
If the use an un-initialized policy to create a launchpad person the
creation_rationale will still be possible, however missing important
information, the upload target:
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-01-13 14:02:28 +0000
@@ -183,7 +183,7 @@
self.invalidate_caches(bug.default_bugtask)
self.getUserBrowser(url, owner)
# At least 20 of these should be removed.
- self.assertThat(recorder, HasQueryCount(LessThan(106)))
+ self.assertThat(recorder, HasQueryCount(LessThan(107)))
count_with_no_branches = recorder.count
for sp in sourcepackages:
self.makeLinkedBranchMergeProposal(sp, bug, owner)
=== modified file 'lib/lp/bugs/doc/externalbugtracker-comment-imports.txt'
--- lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2011-12-29 05:29:36 +0000
+++ lib/lp/bugs/doc/externalbugtracker-comment-imports.txt 2012-01-13 14:02:28 +0000
@@ -179,23 +179,6 @@
>>> bug.messages[-1].owner.name
u'no-priv'
-This also works if the address is associated with an Account, but not a
-Person. This should only happen when the user has logged into ShipIt but
-not Launchpad. A new Person is created.
-
- >>> account = factory.makeAccount(email="account-only@xxxxxxxxxxx")
- >>> external_bugtracker.poster_tuple = (
- ... 'Account Only', 'account-only@xxxxxxxxxxx')
- >>> external_bugtracker.remote_comments['account-only-comment'] = (
- ... "Account-only comment.")
- >>> bugwatch_updater.importBugComments()
- INFO:...:Imported 1 comments for remote bug 123456 on ...
-
- >>> bug.messages[-1].owner.name
- u'account-only'
- >>> bug.messages[-1].owner.account == account
- True
-
It's also possible for Launchpad to create Persons from remote
bugtracker users when the remote bugtracker doesn't specify an email
address. In those cases, the ExternalBugTracker's getPosterForComment()
=== modified file 'lib/lp/bugs/scripts/bugimport.py'
--- lib/lp/bugs/scripts/bugimport.py 2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/scripts/bugimport.py 2012-01-13 14:02:28 +0000
@@ -168,8 +168,9 @@
person = None
if person is None:
- address = getUtility(IEmailAddressSet).getByEmail(email)
- if address is None:
+ person = getUtility(IPersonSet).getByEmail(email)
+
+ if person is None:
self.logger.debug('creating person for %s' % email)
# Has the short name been taken?
if name is not None and (
@@ -184,26 +185,6 @@
rationale=PersonCreationRationale.BUGIMPORT,
comment=('when importing bugs for %s' %
self.product.displayname)))
- elif address.personID is None:
- # The user has an Account and and EmailAddress linked
- # to that account.
- assert address.accountID is not None, (
- "Email address not linked to an Account: %s " % email)
- self.logger.debug(
- 'creating person from account for %s' % email)
- if name is not None and (
- person_set.getByName(name) is not None):
- # The short name is already taken, so we'll pass
- # None to createPerson(), which will take care of
- # creating a unique one.
- name = None
- person = address.account.createPerson(
- rationale=PersonCreationRationale.BUGIMPORT,
- name=name, comment=('when importing bugs for %s' %
- self.product.displayname))
- else:
- # EmailAddress and Person are in different stores.
- person = person_set.get(address.personID)
self.person_id_cache[email] = person.id
=== modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py'
--- lib/lp/bugs/scripts/tests/test_bugimport.py 2012-01-05 00:15:32 +0000
+++ lib/lp/bugs/scripts/tests/test_bugimport.py 2012-01-13 14:02:28 +0000
@@ -297,34 +297,6 @@
self.assertNotEqual(person.preferredemail, None)
self.assertEqual(person.preferredemail.email, 'foo@xxxxxxxxxxxxx')
- def test_person_from_account(self):
- # If an Account record exists for a user's email address, but
- # no Person record is linked to it, the bug importer creates a
- # Person and links the three piece of information together.
- account = self.factory.makeAccount("Sam")
- personnode = ET.fromstring(
- '<person xmlns="https://launchpad.net/xmlns/2006/bugs" />')
- personnode.set('name', generate_nick(account.preferredemail.email))
- personnode.set('email', account.preferredemail.email)
- personnode.text = account.displayname
-
- product = getUtility(IProductSet).getByName('netapplet')
- importer = bugimport.BugImporter(
- product, 'bugs.xml', 'bug-map.pickle', verify_users=True)
- person = importer.getPerson(personnode)
-
- # The person returned is associated with the account.
- self.failUnlessEqual(account.id, person.accountID)
- # The creation comment and rationale are set correctly.
- self.failUnlessEqual(
- 'when importing bugs for %s' % product.displayname,
- person.creation_comment)
- self.failUnlessEqual(
- PersonCreationRationale.BUGIMPORT,
- person.creation_rationale)
- # The person's email addresses are hidden by default.
- self.failUnless(person.hide_email_addresses)
-
class GetMilestoneTestCase(unittest.TestCase):
"""Tests for the BugImporter.getMilestone() method."""
=== modified file 'lib/lp/bugs/templates/buglisting.mustache'
--- lib/lp/bugs/templates/buglisting.mustache 2011-12-20 00:00:03 +0000
+++ lib/lp/bugs/templates/buglisting.mustache 2012-01-13 14:02:28 +0000
@@ -22,60 +22,60 @@
{{/show_id}}
<a href="{{bug_url}}" class="bugtitle">{{title}}</a>
</div>
- </div>
- <div class="buglisting-col3">
- {{#show_targetname}}
- <span class="{{bugtarget_css}} field">
- {{bugtarget}}
- </span>
- {{/show_targetname}}
- {{#show_milestone_name}}
- <span class="sprite milestone field">
- {{#milestone_name}}
- {{milestone_name}}
- {{/milestone_name}}
- {{^milestone_name}}
- No milestone set
- {{/milestone_name}}
- </span>
- {{/show_milestone_name}}
- {{#show_date_last_updated}}
- <span class="sprite milestone field">
- Last updated {{last_updated}}
- </span>
- {{/show_date_last_updated}}
- {{#show_assignee}}
- <span class="sprite person field">
- {{#assignee}}Assignee: {{assignee}}{{/assignee}}
- {{^assignee}}Assignee: None{{/assignee}}
- </span>
- {{/show_assignee}}
- {{#show_reporter}}
- <span class="sprite person field">
- Reporter: {{reporter}}
- </span>
- {{/show_reporter}}
- {{#show_datecreated}}
- <span class="sprite milestone field">
- {{age}}
- </span>
- {{/show_datecreated}}
- {{#show_tag}}
- <span class="field">Tags:
- {{^has_tags}}None{{/has_tags}}
- {{#tags}}
- <a href="{{url}}" class="tag">{{tag}}</a> 
- {{/tags}}
- </span>
- {{/show_tag}}
- {{#show_heat}}
- <span class="bug-heat-icons">
- {{{bug_heat_html}}}
- </span>
- {{/show_heat}}
- <span class="bug-related-icons">
- {{{badges}}}
- </span>
+ <div class="buginfo-extra">
+ {{#show_targetname}}
+ <span class="{{bugtarget_css}} field">
+ {{bugtarget}}
+ </span>
+ {{/show_targetname}}
+ {{#show_milestone_name}}
+ <span class="sprite milestone field">
+ {{#milestone_name}}
+ {{milestone_name}}
+ {{/milestone_name}}
+ {{^milestone_name}}
+ No milestone set
+ {{/milestone_name}}
+ </span>
+ {{/show_milestone_name}}
+ {{#show_date_last_updated}}
+ <span class="sprite milestone field">
+ Last updated {{last_updated}}
+ </span>
+ {{/show_date_last_updated}}
+ {{#show_assignee}}
+ <span class="sprite person field">
+ {{#assignee}}Assignee: {{assignee}}{{/assignee}}
+ {{^assignee}}Assignee: None{{/assignee}}
+ </span>
+ {{/show_assignee}}
+ {{#show_reporter}}
+ <span class="sprite person field">
+ Reporter: {{reporter}}
+ </span>
+ {{/show_reporter}}
+ {{#show_datecreated}}
+ <span class="sprite milestone field">
+ {{age}}
+ </span>
+ {{/show_datecreated}}
+ {{#show_tag}}
+ <span class="field">Tags:
+ {{^has_tags}}None{{/has_tags}}
+ {{#tags}}
+ <a href="{{url}}" class="tag">{{tag}}</a> 
+ {{/tags}}
+ </span>
+ {{/show_tag}}
+ {{#show_heat}}
+ <span class="bug-heat-icons">
+ {{{bug_heat_html}}}
+ </span>
+ {{/show_heat}}
+ <span class="bug-related-icons">
+ {{{badges}}}
+ </span>
+ </div>
</div>
</div>
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2011-12-18 13:45:20 +0000
+++ lib/lp/registry/doc/person-account.txt 2012-01-13 14:02:28 +0000
@@ -31,8 +31,7 @@
the profile. Sample Person cannot claim it.
>>> login('test@xxxxxxxxxxxxx')
- >>> matsubara.account.activate(
- ... comment="test", password='ok', preferred_email=emailaddress)
+ >>> matsubara.account.reactivate(comment="test", password='ok')
Traceback (most recent call last):
...
Unauthorized: ...'launchpad.Special')
@@ -42,8 +41,8 @@
>>> from zope.security.proxy import removeSecurityProxy
>>> login('matsubara@xxxxxxxxxxxx')
- >>> matsubara.account.activate(
- ... comment="test", password='ok', preferred_email=emailaddress)
+ >>> matsubara.account.reactivate(comment="test", password='ok')
+ >>> matsubara.setPreferredEmail(emailaddress)
>>> import transaction
>>> transaction.commit()
>>> matsubara.is_valid_person
@@ -52,7 +51,7 @@
<DBItem AccountStatus.ACTIVE, ...>
>>> matsubara.account.status_comment
u'test'
- >>> removeSecurityProxy(matsubara.account.preferredemail).email
+ >>> removeSecurityProxy(matsubara.preferredemail).email
u'matsubara@xxxxxxxxxxxx'
@@ -212,15 +211,7 @@
Reactivating user accounts
--------------------------
-Accounts can be reactivated. A comment and a non-None preferred email address
-are required to reactivate() an account, though.
-
- >>> foobar.account.reactivate(
- ... 'This will raise an error.', password='', preferred_email=None)
- Traceback (most recent call last):
- ...
- AssertionError: Account ... cannot be activated without a preferred
- email address.
+Accounts can be reactivated.
>>> foobar.reactivate(
... 'User reactivated the account using reset password.',
=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt 2012-01-04 23:49:46 +0000
+++ lib/lp/registry/doc/person.txt 2012-01-13 14:02:28 +0000
@@ -132,9 +132,6 @@
>>> p.account_status
<DBItem AccountStatus.NOACCOUNT...
- >>> email.accountID == p.accountID
- True
-
>>> p.setPreferredEmail(email)
>>> email.status
<DBItem EmailAddressStatus.PREFERRED...
@@ -145,10 +142,7 @@
>>> from lp.services.identity.model.account import Account
>>> from lp.services.database.lpstorm import IMasterStore
>>> account = IMasterStore(Account).get(Account, p.accountID)
- >>> account.activate(
- ... "Activated by doc test.",
- ... password=p.password,
- ... preferred_email=email)
+ >>> account.reactivate("Activated by doc test.", password=p.password)
>>> p.account_status
<DBItem AccountStatus.ACTIVE...
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-01-06 19:56:39 +0000
+++ lib/lp/registry/model/person.py 2012-01-13 14:02:28 +0000
@@ -2540,7 +2540,8 @@
def reactivate(self, comment, password, preferred_email):
"""See `IPersonSpecialRestricted`."""
account = IMasterObject(self.account)
- account.reactivate(comment, password, preferred_email)
+ account.reactivate(comment, password)
+ self.setPreferredEmail(preferred_email)
if '-deactivatedaccount' in self.name:
# The name was changed by deactivateAccount(). Restore the
# name, but we must ensure it does not conflict with a current
@@ -3108,97 +3109,81 @@
# possible replication lag issues but this might actually be
# unnecessary.
with MasterDatabasePolicy():
- store = IMasterStore(EmailAddress)
- join = store.using(
- EmailAddress,
- LeftJoin(Account, EmailAddress.accountID == Account.id))
- email, account = (
- join.find(
- (EmailAddress, Account),
- EmailAddress.email.lower() ==
- ensure_unicode(email_address).lower()).one()
+ email, person = (
+ getUtility(IPersonSet).getByEmails([email_address]).one()
or (None, None))
- identifier = store.find(
+ identifier = IStore(OpenIdIdentifier).find(
OpenIdIdentifier, identifier=openid_identifier).one()
- if email is None and identifier is None:
- # Neither the Email Address not the OpenId Identifier
- # exist in the database. Create the email address,
- # account, and associated info. OpenIdIdentifier is
- # created later.
- account_set = getUtility(IAccountSet)
- account, email = account_set.createAccountAndEmail(
- email_address, creation_rationale, full_name,
- password=None)
- db_updated = True
-
- elif email is None:
- # The Email Address does not exist in the database,
- # but the OpenId Identifier does. Create the Email
- # Address and link it to the account.
- assert account is None, 'Retrieved an account but not email?'
- account = identifier.account
- emailaddress_set = getUtility(IEmailAddressSet)
- email = emailaddress_set.new(
- email_address, account=account)
- db_updated = True
-
- elif account is None:
- # Email address exists, but there is no Account linked
- # to it. Create the Account and link it to the
- # EmailAddress.
+ if email is None:
+ if identifier is None:
+ # Neither the Email Address not the OpenId Identifier
+ # exist in the database. Create the email address,
+ # account, and associated info. OpenIdIdentifier is
+ # created later.
+ person_set = getUtility(IPersonSet)
+ person, email = person_set.createPersonAndEmail(
+ email_address, creation_rationale, comment=comment,
+ displayname=full_name)
+ db_updated = True
+ else:
+ # The Email Address does not exist in the database,
+ # but the OpenId Identifier does. Create the Email
+ # Address and link it to the person.
+ person = IPerson(identifier.account, None)
+ assert person is not None, (
+ 'Received a personless account.')
+ emailaddress_set = getUtility(IEmailAddressSet)
+ email = emailaddress_set.new(email_address, person=person)
+ db_updated = True
+ elif email.person.account is None:
+ # Email address and person exist, but there is no
+ # account. Create and link it.
account_set = getUtility(IAccountSet)
account = account_set.new(
AccountCreationRationale.OWNER_CREATED_LAUNCHPAD,
full_name)
- email.account = account
+ removeSecurityProxy(email.person).account = account
db_updated = True
+ person = email.person
+ assert person.account is not None
+
if identifier is None:
# This is the first time we have seen that
# OpenIdIdentifier. Link it.
identifier = OpenIdIdentifier()
- identifier.account = account
+ identifier.account = person.account
identifier.identifier = openid_identifier
- store.add(identifier)
+ IStore(OpenIdIdentifier).add(identifier)
db_updated = True
-
- elif identifier.account != account:
+ elif identifier.account != person.account:
# The ISD OpenId server may have linked this OpenId
# identifier to a new email address, or the user may
# have transfered their email address to a different
# Launchpad Account. If that happened, repair the
# link - we trust the ISD OpenId server.
- identifier.account = account
+ identifier.account = person.account
db_updated = True
# We now have an account, email address, and openid identifier.
- if account.status == AccountStatus.SUSPENDED:
+ if person.account.status == AccountStatus.SUSPENDED:
raise AccountSuspendedError(
"The account matching the identifier is suspended.")
- elif account.status in [AccountStatus.DEACTIVATED,
- AccountStatus.NOACCOUNT]:
- password = '' # Needed just to please reactivate() below.
- removeSecurityProxy(account).reactivate(
- comment, password, removeSecurityProxy(email))
+ elif person.account.status in [AccountStatus.DEACTIVATED,
+ AccountStatus.NOACCOUNT]:
+ password = ''
+ removeSecurityProxy(person.account).reactivate(
+ comment, password)
+ removeSecurityProxy(person).setPreferredEmail(email)
db_updated = True
else:
# Account is active, so nothing to do.
pass
- if IPerson(account, None) is None:
- removeSecurityProxy(account).createPerson(
- creation_rationale, comment=comment)
- db_updated = True
-
- person = IPerson(account)
- if email.personID != person.id:
- removeSecurityProxy(email).person = person
- db_updated = True
-
- return person, db_updated
+ return email.person, db_updated
def newTeam(self, teamowner, name, displayname, teamdescription=None,
subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
@@ -3251,12 +3236,8 @@
person = self._newPerson(
name, displayname, hide_email_addresses, rationale=rationale,
comment=comment, registrant=registrant, account=account)
-
- email = getUtility(IEmailAddressSet).new(
- email, person, account=account)
-
- assert email.accountID is not None, (
- 'Failed to link EmailAddress to Account')
+ email = getUtility(IEmailAddressSet).new(email, person)
+
return person, email
def createPersonWithoutEmail(
@@ -3299,55 +3280,14 @@
def ensurePerson(self, email, displayname, rationale, comment=None,
registrant=None):
"""See `IPersonSet`."""
- # Start by checking if there is an `EmailAddress` for the given
- # text address. There are many cases where an email address can be
- # created without an associated `Person`. For instance, we created
- # an account linked to the address through an external system such
- # SSO or ShipIt.
- email_address = getUtility(IEmailAddressSet).getByEmail(email)
+ person = getUtility(IPersonSet).getByEmail(email)
- # There is no `EmailAddress` for this text address, so we need to
- # create both the `Person` and `EmailAddress` here and we are done.
- if email_address is None:
+ if person is None:
person, email_address = self.createPersonAndEmail(
email, rationale, comment=comment, displayname=displayname,
registrant=registrant, hide_email_addresses=True)
- return person
-
- # There is an `EmailAddress` for this text address, but no
- # associated `Person`.
- if email_address.person is None:
- assert email_address.accountID is not None, (
- '%s is not associated to a person or account'
- % email_address.email)
- account = IMasterStore(Account).get(
- Account, email_address.accountID)
- account_person = self.getByAccount(account)
- if account_person is None:
- # There is no associated `Person` to the email `Account`.
- # This is probably because the account was created externally
- # to Launchpad. Create just the `Person`, associate it with
- # the `EmailAddress` and return it.
- name = generate_nick(email)
- account_person = self._newPerson(
- name, displayname, hide_email_addresses=True,
- rationale=rationale, comment=comment,
- registrant=registrant, account=email_address.account)
- # There is (now) a `Person` linked to the `Account`, link the
- # `EmailAddress` to this `Person` and return it.
- master_email = IMasterStore(EmailAddress).get(
- EmailAddress, email_address.id)
- master_email.personID = account_person.id
- # Populate the previously empty 'preferredemail' cached
- # property, so the Person record is up-to-date.
- if master_email.status == EmailAddressStatus.PREFERRED:
- cache = get_property_cache(account_person)
- cache.preferredemail = master_email
- return account_person
-
- # Easy, return the `Person` associated with the existing
- # `EmailAddress`.
- return IMasterStore(Person).get(Person, email_address.personID)
+
+ return person
def getByName(self, name, ignore_merged=True):
"""See `IPersonSet`."""
=== modified file 'lib/lp/registry/tests/test_mailinglistapi.py'
--- lib/lp/registry/tests/test_mailinglistapi.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/tests/test_mailinglistapi.py 2012-01-13 14:02:28 +0000
@@ -73,10 +73,6 @@
def test_isRegisteredInLaunchpad_email_no_email_address(self):
self.assertFalse(self.api.isRegisteredInLaunchpad('me@xxxxxxxxx'))
- def test_isRegisteredInLaunchpad_email_without_person(self):
- self.factory.makeAccount('Me', email='me@xxxxxxxxx')
- self.assertFalse(self.api.isRegisteredInLaunchpad('me@xxxxxxxxx'))
-
def test_isRegisteredInLaunchpad_archive_address_is_false(self):
# The Mailman archive address can never be owned by an Lp user
# because such a user would have acces to all lists.
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2012-01-06 15:14:48 +0000
+++ lib/lp/registry/tests/test_personset.py 2012-01-13 14:02:28 +0000
@@ -229,7 +229,6 @@
email = from_person.preferredemail
email.status = EmailAddressStatus.NEW
email.person = to_person
- email.account = to_person.account
transaction.commit()
def _do_merge(self, from_person, to_person, reviewer=None):
@@ -725,12 +724,10 @@
# The old email address is still there and correctly linked.
self.assertIs(self.email, found.preferredemail)
- self.assertIs(self.email.account, self.account)
self.assertIs(self.email.person, self.person)
# The new email address is there too and correctly linked.
new_email = self.store.find(EmailAddress, email=new_email).one()
- self.assertIs(new_email.account, self.account)
self.assertIs(new_email.person, self.person)
self.assertEqual(EmailAddressStatus.NEW, new_email.status)
@@ -751,32 +748,9 @@
# It is correctly linked to an account, emailaddress and
# identifier.
self.assertIs(found, found.preferredemail.person)
- self.assertIs(found.account, found.preferredemail.account)
self.assertEqual(
new_identifier, found.account.openid_identifiers.any().identifier)
- def testNoPerson(self):
- # If the account is not linked to a Person, create one. ShipIt
- # users fall into this category the first time they log into
- # Launchpad.
- self.email.person = None
- self.person.account = None
-
- found, updated = self.person_set.getOrCreateByOpenIDIdentifier(
- self.identifier.identifier, self.email.email, 'New Name',
- PersonCreationRationale.UNKNOWN, 'No Comment')
- found = removeSecurityProxy(found)
-
- # We have a new Person
- self.assertIs(True, updated)
- self.assertIsNot(self.person, found)
-
- # It is correctly linked to an account, emailaddress and
- # identifier.
- self.assertIs(found, found.preferredemail.person)
- self.assertIs(found.account, found.preferredemail.account)
- self.assertIn(self.identifier, list(found.account.openid_identifiers))
-
def testNoAccount(self):
# EmailAddress is linked to a Person, but there is no Account.
# Convert this stub into something valid.
@@ -795,7 +769,6 @@
self.assertEqual(
new_identifier, found.account.openid_identifiers.any().identifier)
self.assertIs(self.email.person, found)
- self.assertIs(self.email.account, found.account)
self.assertEqual(EmailAddressStatus.PREFERRED, self.email.status)
def testMovedEmailAddress(self):
@@ -916,41 +889,6 @@
self.email_address, self.displayname, self.rationale)
self.assertTrue(ensured_person.hide_email_addresses)
- def test_ensurePerson_for_existing_account(self):
- # IPerson.ensurePerson creates missing Person for existing
- # Accounts.
- test_account = self.factory.makeAccount(
- self.displayname, email=self.email_address)
- self.assertIs(None, test_account.preferredemail.person)
-
- ensured_person = self.person_set.ensurePerson(
- self.email_address, self.displayname, self.rationale)
- self.assertEquals(test_account.id, ensured_person.account.id)
- self.assertEquals(
- test_account.preferredemail, ensured_person.preferredemail)
- self.assertEquals(ensured_person, test_account.preferredemail.person)
- self.assertTrue(ensured_person.hide_email_addresses)
-
- def test_ensurePerson_for_existing_account_with_person(self):
- # IPerson.ensurePerson return existing Person for existing
- # Accounts and additionally bounds the account email to the
- # Person in question.
-
- # Create a testing `Account` and a testing `Person` directly,
- # linked.
- testing_account = self.factory.makeAccount(
- self.displayname, email=self.email_address)
- testing_person = removeSecurityProxy(
- testing_account).createPerson(self.rationale)
- self.assertEqual(
- testing_person, testing_account.preferredemail.person)
-
- # Since there's an existing Person for the given email address,
- # IPersonSet.ensurePerson() will just return it.
- ensured_person = self.person_set.ensurePerson(
- self.email_address, self.displayname, self.rationale)
- self.assertEqual(testing_person, ensured_person)
-
class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory):
@@ -977,25 +915,6 @@
self.assertEqual(person, result)
self.assertFalse(db_updated)
- def test_existing_account_no_person(self):
- # A person is created with the correct rationale.
- account = self.factory.makeAccount('purchaser')
- openid_ident = removeSecurityProxy(
- account).openid_identifiers.any().identifier
-
- person, db_updated = self.callGetOrCreate(openid_ident)
-
- self.assertEqual(account, person.account)
- # The person is created with the correct rationale and creation
- # comment.
- self.assertEqual(
- "when purchasing an application via Software Center.",
- person.creation_comment)
- self.assertEqual(
- PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
- person.creation_rationale)
- self.assertTrue(db_updated)
-
def test_existing_deactivated_account(self):
# An existing deactivated account will be reactivated.
person = self.factory.makePerson(
=== modified file 'lib/lp/registry/xmlrpc/mailinglist.py'
--- lib/lp/registry/xmlrpc/mailinglist.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/xmlrpc/mailinglist.py 2012-01-13 14:02:28 +0000
@@ -225,7 +225,6 @@
return False
email_address = getUtility(IEmailAddressSet).getByEmail(address)
return (email_address is not None and
- email_address.personID is not None and
not email_address.person.is_team and
email_address.status in (EmailAddressStatus.VALIDATED,
EmailAddressStatus.PREFERRED))
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2012-01-06 18:59:36 +0000
+++ lib/lp/scripts/garbo.py 2012-01-13 14:02:28 +0000
@@ -701,7 +701,7 @@
AND Person.id IN (%s)
""" % people_ids)
self.store.execute("""
- UPDATE EmailAddress SET person=NULL
+ DELETE FROM EmailAddress
WHERE person IN (%s)
""" % people_ids)
# This cascade deletes any PersonSettings records.
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-01-06 17:35:41 +0000
+++ lib/lp/security.py 2012-01-13 14:02:28 +0000
@@ -147,7 +147,6 @@
from lp.registry.interfaces.role import (
IHasDrivers,
IHasOwner,
- IPersonRoles,
)
from lp.registry.interfaces.sourcepackage import ISourcePackage
from lp.registry.interfaces.teammembership import (
@@ -168,6 +167,7 @@
IOAuthAccessToken,
IOAuthRequestToken,
)
+from lp.services.webapp.authorization import check_permission
from lp.services.openid.interfaces.openididentifier import IOpenIdIdentifier
from lp.services.webapp.interfaces import ILaunchpadRoot
from lp.services.worlddata.interfaces.country import ICountry
@@ -237,6 +237,30 @@
return True
+class LimitedViewDeferredToView(AuthorizationBase):
+ """The default ruleset for the launchpad.LimitedView permission.
+
+ Few objects define LimitedView permission because it is only needed
+ in cases where a user may know something about a private object. The
+ default behaviour is to check if the user has launchpad.View permission;
+ private objects must define their own launchpad.LimitedView checker to
+ trully check the permission.
+ """
+ permission = 'launchpad.LimitedView'
+ usedfor = Interface
+
+ def checkUnauthenticated(self):
+ # The forward adapter approach is not reliable because the object
+ # might not define a permission checker for launchpad.View.
+ # eg. IHasMilestones is implicitly public to anonymous users,
+ # there is no nearest adapter to call checkUnauthenticated.
+ return check_permission('launchpad.View', self.obj)
+
+ def checkAuthenticated(self, user):
+ return self.forwardCheckAuthenticated(
+ user, self.obj, 'launchpad.View')
+
+
class AdminByAdminsTeam(AuthorizationBase):
permission = 'launchpad.Admin'
usedfor = Interface
@@ -2632,7 +2656,7 @@
# Anonymous users can never see email addresses.
return False
- def checkAccountAuthenticated(self, account):
+ def checkAuthenticated(self, user):
"""Can the user see the details of this email address?
If the email address' owner doesn't want his email addresses to be
@@ -2640,17 +2664,13 @@
admins can see them.
"""
# Always allow users to see their own email addresses.
- if self.obj.account == account:
+ if self.obj.person == user:
return True
if not (self.obj.person is None or
self.obj.person.hide_email_addresses):
return True
- user = IPersonRoles(IPerson(account, None), None)
- if user is None:
- return False
-
return (self.obj.person is not None and user.inTeam(self.obj.person)
or user.in_commercial_admin
or user.in_registry_experts
@@ -2661,12 +2681,11 @@
permission = 'launchpad.Edit'
usedfor = IEmailAddress
- def checkAccountAuthenticated(self, account):
+ def checkAuthenticated(self, user):
# Always allow users to see their own email addresses.
- if self.obj.account == account:
+ if self.obj.person == user:
return True
- return super(EditEmailAddress, self).checkAccountAuthenticated(
- account)
+ return super(EditEmailAddress, self).checkAuthenticated(user)
class ViewGPGKey(AnonymousAuthorization):
=== modified file 'lib/lp/services/identity/doc/account.txt'
--- lib/lp/services/identity/doc/account.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/services/identity/doc/account.txt 2012-01-13 14:02:28 +0000
@@ -13,7 +13,7 @@
implements the IAccountSet interface.
>>> from zope.interface.verify import verifyObject
- >>> from lp.registry.interfaces.person import IPerson
+ >>> from lp.registry.interfaces.person import IPersonSet
>>> from lp.services.identity.interfaces.account import (
... IAccount, IAccountSet)
@@ -21,37 +21,8 @@
>>> verifyObject(IAccountSet, account_set)
True
-
-Looking up accounts by email address
-------------------------------------
-
-Accounts are generally looked up by email address.
-
- >>> login('no-priv@xxxxxxxxxxxxx')
- >>> account = account_set.getByEmail('no-priv@xxxxxxxxxxxxx')
- >>> IAccount.providedBy(account)
- True
-
-If the account is not found, a LookupError is raised.
-
- >>> account_set.getByEmail('invalid@whatever')
- Traceback (most recent call last):
- ...
- LookupError:...
-
-Only admins or the person attached to an account can see or edit Account
-details. This is obviously wrong, as the account should have access
-rather than the (optional) attached person. In particular, it means
-Accounts without Person records cannot be managed by the Account owner.
-Fixing this involves more surgery to Launchpad's security systems.
-
- >>> stub_account = account_set.getByEmail('stuart.bishop@xxxxxxxxxxxxx')
- >>> stub_account.date_created
- Traceback (most recent call last):
- ...
- Unauthorized...
-
- >>> del stub_account
+ >>> account = getUtility(IPersonSet).getByEmail(
+ ... 'no-priv@xxxxxxxxxxxxx').account
Looking up accounts by their database ID
@@ -102,49 +73,16 @@
True
>>> login('no-priv@xxxxxxxxxxxxx')
-An account has a displayname, and a preferred email address.
+An account has a displayname.
>>> print account.displayname
No Privileges Person
- >>> print account.preferredemail.email
- no-priv@xxxxxxxxxxxxx
Account objects have a useful string representation.
>>> account
<Account 'No Privileges Person' (Active account)>
-The account can have additional validated and guessed email
-addresses. This will be empty if the user has only a single validated
-email address.
-
- >>> [email.email for email in account.validated_emails]
- []
- >>> [email.email for email in account.guessed_emails]
- []
-
-If we add a new guessed email address, it will be included in the
-guessed list.
-
- >>> from lp.services.identity.interfaces.emailaddress import (
- ... EmailAddressStatus,
- ... IEmailAddressSet,
- ... )
- >>> email = getUtility(IEmailAddressSet).new(
- ... "guessed-email@xxxxxxxxxxx", account=account,
- ... status=EmailAddressStatus.NEW)
- >>> [email.email for email in account.guessed_emails]
- [u'guessed-email@xxxxxxxxxxx']
-
-If we add a validated email address, it will show up in the validated
-list.
-
- >>> email = getUtility(IEmailAddressSet).new(
- ... "validated-email@xxxxxxxxxxx", account=account,
- ... status=EmailAddressStatus.VALIDATED)
- >>> [email.email for email in account.validated_emails]
- [u'validated-email@xxxxxxxxxxx']
-
It also has an encrypted password.
>>> print account.password
@@ -226,8 +164,6 @@
Passwordless
>>> print passwordless_account.password
None
- >>> print passwordless_account.preferredemail
- None
The new() method accepts the optional parameters of password and
password_is_encrypted. If password_is_encrypted is False, the default,
@@ -249,95 +185,3 @@
>>> Store.of(clear_account).flush()
>>> print clear_account.password
clear_password
-
-
-Valid Accounts
---------------
-
-Like person objects, an account is considered valid if it is in the
-active state and has a preferred email address. So a newly created
-account with no email address is not valid.
-
- >>> account = account_set.new(
- ... AccountCreationRationale.USER_CREATED,
- ... "Valid Account Test")
- >>> account.status = AccountStatus.ACTIVE
- >>> account.is_valid
- False
-
-Let's add a new email address to the account.
-
- >>> email = getUtility(IEmailAddressSet).new(
- ... "valid-account-test@xxxxxxxxxxx", account=account)
- >>> account.is_valid
- False
-
-The account is still not valid because it has no preferred email.
-Setting the email to preferred fixes this.
-
- >>> from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
- >>> email.status = EmailAddressStatus.PREFERRED
- >>> account.is_valid
- True
-
-If the account is deactivated, it won't be considered valid any more:
-
- >>> account.status = AccountStatus.DEACTIVATED
- >>> account.is_valid
- False
-
-
-Creating an IPerson for an Account
-----------------------------------
-
-Newly created accounts without an associated Person can be 'promoted' to full
-Launchpad accounts with an attached Person.
-
- # We need to change database policy here again, as the SSO Server cannot
- # modify tables in the lpmain replication set.
- >>> from lp.services.webapp.dbpolicy import MasterDatabasePolicy
- >>> from lp.services.webapp.interfaces import IStoreSelector
- >>> getUtility(IStoreSelector).push(MasterDatabasePolicy())
-
- >>> from lp.registry.interfaces.person import PersonCreationRationale
- >>> fresh_account, email = account_set.createAccountAndEmail(
- ... 'foo@xxxxxxxxxxx',
- ... AccountCreationRationale.OWNER_CREATED_UBUNTU_SHOP,
- ... 'Display name', 'password')
- >>> IPerson(fresh_account)
- Traceback (most recent call last):
- ...
- TypeError: ('Could not adapt', ...
-
- >>> person = fresh_account.createPerson(
- ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
- >>> transaction.commit()
- >>> person.account == fresh_account
- True
- >>> IPerson(fresh_account) == person
- True
- >>> person.preferredemail == fresh_account.preferredemail
- True
- >>> person.creation_rationale
- <DBItem PersonCreationRationale.OWNER_CREATED_LAUNCHPAD...
-
-However, if the account has an associated person or has no preferred email
-address, a new Person cannot be created.
-
- >>> person = fresh_account.createPerson(
- ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
- Traceback (most recent call last):
- ...
- AssertionError: Can't create a Person for an account which already has
- one.
-
- >>> print clear_account.preferredemail
- None
- >>> person = clear_account.createPerson(
- ... PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
- Traceback (most recent call last):
- ...
- AssertionError: Can't create a Person for an account which has no email.
-
- >>> db_policy = getUtility(IStoreSelector).pop()
-
=== modified file 'lib/lp/services/identity/interfaces/account.py'
--- lib/lp/services/identity/interfaces/account.py 2011-12-24 16:54:44 +0000
+++ lib/lp/services/identity/interfaces/account.py 2012-01-13 14:02:28 +0000
@@ -23,21 +23,15 @@
DBEnumeratedType,
DBItem,
)
-from lazr.restful.fields import (
- CollectionField,
- Reference,
- )
from zope.interface import (
Attribute,
Interface,
)
from zope.schema import (
- Bool,
Choice,
Datetime,
Int,
Text,
- TextLine,
)
from lp import _
@@ -223,58 +217,6 @@
title=_("The status of this account"), required=True,
readonly=False, vocabulary=AccountStatus)
- is_valid = Bool(
- title=_("True if this account is active and has a valid email."),
- required=True, readonly=True)
-
- # We should use schema=IEmailAddress here, but we can't because that would
- # cause circular dependencies.
- preferredemail = Reference(
- title=_("Preferred email address"),
- description=_("The preferred email address for this person. "
- "The one we'll use to communicate with them."),
- readonly=True, required=False, schema=Interface)
-
- validated_emails = CollectionField(
- title=_("Confirmed e-mails of this account."),
- description=_(
- "Confirmed e-mails are the ones in the VALIDATED state. The "
- "user has confirmed that they are active and that they control "
- "them."),
- readonly=True, required=False,
- value_type=Reference(schema=Interface))
-
- guessed_emails = CollectionField(
- title=_("Guessed e-mails of this account."),
- description=_(
- "Guessed e-mails are the ones in the NEW state. We believe "
- "that the user owns the address, but they have not confirmed "
- "the fact."),
- readonly=True, required=False,
- value_type=Reference(schema=Interface))
-
- def setPreferredEmail(email):
- """Set the given email address as this account's preferred one.
-
- If ``email`` is None, the preferred email address is unset, which
- will make the account invalid.
- """
-
- def validateAndEnsurePreferredEmail(email):
- """Ensure this account has a preferred email.
-
- If this account doesn't have a preferred email, <email> will be set as
- this account's preferred one. Otherwise it'll be set as VALIDATED and
- this account will keep their old preferred email.
-
- This method is meant to be the only one to change the status of an
- email address, but as we all know the real world is far from ideal
- and we have to deal with this in one more place, which is the case
- when people explicitly want to change their preferred email address.
- On that case, though, all we have to do is use
- account.setPreferredEmail().
- """
-
class IAccountPrivate(Interface):
"""Private information on an `IAccount`."""
@@ -290,16 +232,6 @@
password = PasswordField(
title=_("Password."), readonly=False, required=True)
- def createPerson(rationale, name=None, comment=None):
- """Create and return a new `IPerson` associated with this account.
-
- :param rationale: A member of `AccountCreationRationale`.
- :param name: Specify a name for the `IPerson` instead of
- using an automatically generated one.
- :param comment: Populate `IPerson.creation_comment`. See
- `IPerson`.
- """
-
class IAccountSpecialRestricted(Interface):
"""Attributes of `IAccount` protected with launchpad.Special."""
@@ -312,12 +244,7 @@
title=_("Why are you deactivating your account?"),
required=False, readonly=False)
- # XXX sinzui 2008-07-14 bug=248518:
- # This method would assert the password is not None, but
- # setPreferredEmail() passes the Person's current password, which may
- # be None. Once that callsite is fixed, we will be able to check that the
- # password is not None here and get rid of the reactivate() method below.
- def activate(comment, password, preferred_email):
+ def reactivate(comment, password):
"""Activate this account.
Set the account status to ACTIVE, the account's password to the given
@@ -325,15 +252,6 @@
:param comment: An explanation of why the account status changed.
:param password: The user's password.
- :param preferred_email: The `EmailAddress` to set as the account's
- preferred email address. It cannot be None.
- """
-
- def reactivate(comment, password, preferred_email):
- """Reactivate this account.
-
- Just like `IAccountSpecialRestricted`.activate() above, but here the
- password can't be None or the empty string.
"""
@@ -364,24 +282,6 @@
:raises LookupError: If the account is not found.
"""
- def createAccountAndEmail(email, rationale, displayname, password,
- password_is_encrypted=False):
- """Create and return both a new `IAccount` and `IEmailAddress`.
-
- The account will be in the ACTIVE state, with the email address set as
- its preferred email address.
- """
-
- def getByEmail(email):
- """Return the `IAccount` linked to the given email address.
-
- :param email: A string, not an `IEmailAddress` provider.
-
- :return: An `IAccount`.
-
- :raises LookupError: If the account is not found.
- """
-
def getByOpenIDIdentifier(openid_identity):
"""Return the `IAccount` with the given OpenID identifier.
@@ -390,4 +290,3 @@
:return: An `IAccount`
:raises LookupError: If the account is not found.
"""
-
=== modified file 'lib/lp/services/identity/interfaces/emailaddress.py'
--- lib/lp/services/identity/interfaces/emailaddress.py 2012-01-05 00:15:32 +0000
+++ lib/lp/services/identity/interfaces/emailaddress.py 2012-01-13 14:02:28 +0000
@@ -32,7 +32,6 @@
from lp import _
from lp.registry.interfaces.role import IHasOwner
-from lp.services.identity.interfaces.account import IAccount
class InvalidEmailAddress(Exception):
@@ -99,8 +98,6 @@
status = Choice(
title=_('Email Address Status'), required=True, readonly=False,
vocabulary=EmailAddressStatus)
- account = Object(title=_('Account'), schema=IAccount, required=False)
- accountID = Int(title=_('AccountID'), required=False, readonly=True)
person = exported(
Reference(title=_('Person'), required=False, readonly=False,
schema=Interface))
@@ -131,12 +128,10 @@
class IEmailAddressSet(Interface):
"""The set of EmailAddresses."""
- def new(email, person=None, status=EmailAddressStatus.NEW, account=None):
+ def new(email, person=None, status=EmailAddressStatus.NEW):
"""Create a new EmailAddress with the given email.
- The newly created EmailAddress will point to the person
- and/or account. If account is omitted and the person has a linked
- account, that account will be used.
+ The newly created EmailAddress will point to the person.
The given status must be an item of EmailAddressStatus.
=== modified file 'lib/lp/services/identity/model/account.py'
--- lib/lp/services/identity/model/account.py 2011-12-30 06:14:56 +0000
+++ lib/lp/services/identity/model/account.py 2012-01-13 14:02:28 +0000
@@ -15,16 +15,13 @@
StringCol,
)
from storm.locals import ReferenceSet
-from storm.store import Store
from zope.component import getUtility
from zope.interface import implements
-from zope.security.proxy import removeSecurityProxy
from lp.services.database.constants import UTC_NOW
from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import EnumCol
from lp.services.database.lpstorm import (
- IMasterObject,
IMasterStore,
IStore,
)
@@ -35,12 +32,6 @@
IAccount,
IAccountSet,
)
-from lp.services.identity.interfaces.emailaddress import (
- EmailAddressStatus,
- IEmailAddress,
- IEmailAddressSet,
- )
-from lp.services.identity.model.emailaddress import EmailAddress
from lp.services.openid.model.openididentifier import OpenIdIdentifier
from lp.services.webapp.interfaces import IPasswordEncryptor
@@ -70,100 +61,11 @@
return "<%s '%s' (%s)>" % (
self.__class__.__name__, displayname, self.status)
- def _getEmails(self, status):
- """Get related `EmailAddress` objects with the given status."""
- result = IStore(EmailAddress).find(
- EmailAddress, accountID=self.id, status=status)
- result.order_by(EmailAddress.email.lower())
- return result
-
- @property
- def preferredemail(self):
- """See `IAccount`."""
- return self._getEmails(EmailAddressStatus.PREFERRED).one()
-
- @property
- def validated_emails(self):
- """See `IAccount`."""
- return self._getEmails(EmailAddressStatus.VALIDATED)
-
- @property
- def guessed_emails(self):
- """See `IAccount`."""
- return self._getEmails(EmailAddressStatus.NEW)
-
- def setPreferredEmail(self, email):
- """See `IAccount`."""
- if email is None:
- # Mark preferred email address as validated, if it exists.
- # XXX 2009-03-30 jamesh bug=349482: we should be able to
- # use ResultSet.set() here :(
- for address in self._getEmails(EmailAddressStatus.PREFERRED):
- address.status = EmailAddressStatus.VALIDATED
- return
-
- if not IEmailAddress.providedBy(email):
- raise TypeError("Any person's email address must provide the "
- "IEmailAddress Interface. %r doesn't." % email)
-
- email = IMasterObject(removeSecurityProxy(email))
- assert email.accountID == self.id
-
- # If we have the preferred email address here, we're done.
- if email.status == EmailAddressStatus.PREFERRED:
- return
-
- existing_preferred_email = self.preferredemail
- if existing_preferred_email is not None:
- assert Store.of(email) is Store.of(existing_preferred_email), (
- "Store of %r is not the same as store of %r" %
- (email, existing_preferred_email))
- existing_preferred_email.status = EmailAddressStatus.VALIDATED
- # Make sure the old preferred email gets flushed before
- # setting the new preferred email.
- Store.of(email).add_flush_order(existing_preferred_email, email)
-
- email.status = EmailAddressStatus.PREFERRED
-
- def validateAndEnsurePreferredEmail(self, email):
- """See `IAccount`."""
- if not IEmailAddress.providedBy(email):
- raise TypeError(
- "Any person's email address must provide the IEmailAddress "
- "interface. %s doesn't." % email)
-
- assert email.accountID == self.id, 'Wrong account! %r, %r' % (
- email.accountID, self.id)
-
- # This email is already validated and is this person's preferred
- # email, so we have nothing to do.
- if email.status == EmailAddressStatus.PREFERRED:
- return
-
- email = IMasterObject(email)
-
- if self.preferredemail is None:
- # This branch will be executed only in the first time a person
- # uses Launchpad. Either when creating a new account or when
- # resetting the password of an automatically created one.
- self.setPreferredEmail(email)
- else:
- email.status = EmailAddressStatus.VALIDATED
-
- def activate(self, comment, password, preferred_email):
+ def reactivate(self, comment, password):
"""See `IAccountSpecialRestricted`."""
- if preferred_email is None:
- raise AssertionError(
- "Account %s cannot be activated without a "
- "preferred email address." % self.id)
self.status = AccountStatus.ACTIVE
self.status_comment = comment
self.password = password
- self.validateAndEnsurePreferredEmail(preferred_email)
-
- def reactivate(self, comment, password, preferred_email):
- """See `IAccountSpecialRestricted`."""
- self.activate(comment, password, preferred_email)
# The password is actually stored in a separate table for security
# reasons, so use a property to hide this implementation detail.
@@ -201,39 +103,6 @@
password = property(_get_password, _set_password)
- @property
- def is_valid(self):
- """See `IAccount`."""
- if self.status != AccountStatus.ACTIVE:
- return False
- return self.preferredemail is not None
-
- def createPerson(self, rationale, name=None, comment=None):
- """See `IAccount`."""
- # Need a local import because of circular dependencies.
- from lp.registry.model.person import (
- generate_nick, Person, PersonSet)
- assert self.preferredemail is not None, (
- "Can't create a Person for an account which has no email.")
- person = IMasterStore(Person).find(Person, accountID=self.id).one()
- assert person is None, (
- "Can't create a Person for an account which already has one.")
- if name is None:
- name = generate_nick(self.preferredemail.email)
- person = PersonSet()._newPerson(
- name, self.displayname, hide_email_addresses=True,
- rationale=rationale, account=self, comment=comment)
-
- # Update all associated email addresses to point at the new person.
- result = IMasterStore(EmailAddress).find(
- EmailAddress, accountID=self.id)
- # XXX 2009-03-30 jamesh bug=349482: we should be able to
- # use ResultSet.set() here :(
- for email in result:
- email.personID = person.id
-
- return person
-
class AccountSet:
"""See `IAccountSet`."""
@@ -269,39 +138,6 @@
raise LookupError(id)
return account
- def createAccountAndEmail(self, email, rationale, displayname, password,
- password_is_encrypted=False,
- openid_identifier=None):
- """See `IAccountSet`."""
- # Convert the PersonCreationRationale to an AccountCreationRationale.
- account_rationale = getattr(AccountCreationRationale, rationale.name)
- account = self.new(
- account_rationale, displayname, password=password,
- password_is_encrypted=password_is_encrypted,
- openid_identifier=openid_identifier)
- account.status = AccountStatus.ACTIVE
- email = getUtility(IEmailAddressSet).new(
- email, status=EmailAddressStatus.PREFERRED, account=account)
- return account, email
-
- def getByEmail(self, email):
- """See `IAccountSet`."""
- store = IStore(Account)
- try:
- email = email.decode('US-ASCII')
- except (UnicodeDecodeError, UnicodeEncodeError):
- # Non-ascii email addresses are not legal, so assume there are no
- # matching addresses in Launchpad.
- raise LookupError(repr(email))
- account = store.find(
- Account,
- EmailAddress.account == Account.id,
- EmailAddress.email.lower()
- == email.strip().lower()).one()
- if account is None:
- raise LookupError(email)
- return account
-
def getByOpenIDIdentifier(self, openid_identifier):
"""See `IAccountSet`."""
store = IStore(Account)
=== modified file 'lib/lp/services/identity/model/emailaddress.py'
--- lib/lp/services/identity/model/emailaddress.py 2012-01-05 00:15:32 +0000
+++ lib/lp/services/identity/model/emailaddress.py 2012-01-13 14:02:28 +0000
@@ -56,9 +56,6 @@
dbName='email', notNull=True, unique=True, alternateID=True)
status = EnumCol(dbName='status', schema=EmailAddressStatus, notNull=True)
person = ForeignKey(dbName='person', foreignKey='Person', notNull=False)
- account = ForeignKey(
- dbName='account', foreignKey='Account', notNull=False,
- default=None)
def __repr__(self):
return '<EmailAddress at 0x%x <%s> [%s]>' % (
@@ -116,8 +113,7 @@
return EmailAddress.selectOne(
"lower(email) = %s" % quote(email.strip().lower()))
- def new(self, email, person=None, status=EmailAddressStatus.NEW,
- account=None):
+ def new(self, email, person=None, status=EmailAddressStatus.NEW):
"""See IEmailAddressSet."""
email = email.strip()
@@ -129,26 +125,11 @@
raise EmailAddressAlreadyTaken(
"The email address '%s' is already registered." % email)
assert status in EmailAddressStatus.items
- if person is None:
- personID = None
- else:
- if account is None:
- account = person.account
- personID = person.id
- accountID = account and account.id
- assert person.accountID == accountID, (
- "Email address '%s' must be linked to same account as "
- "person '%s'. Expected %r (%s), got %r (%s)" % (
- email, person.name, person.account, person.accountID,
- account, accountID))
- # We use personID instead of just person, as in some cases the
- # Person record will not yet be replicated from the main
- # Store to the auth master Store.
+ assert person
return EmailAddress(
email=email,
status=status,
- personID=personID,
- account=account)
+ person=person)
class UndeletableEmailAddress(Exception):
=== modified file 'lib/lp/services/identity/tests/test_account.py'
--- lib/lp/services/identity/tests/test_account.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/identity/tests/test_account.py 2012-01-13 14:02:28 +0000
@@ -6,25 +6,7 @@
__metaclass__ = type
__all__ = []
-from testtools.testcase import ExpectedException
-import transaction
-from zope.component import getUtility
-
-from lp.registry.interfaces.person import (
- IPerson,
- PersonCreationRationale,
- )
-from lp.services.identity.interfaces.account import (
- AccountCreationRationale,
- IAccountSet,
- )
-from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
-from lp.services.webapp.authorization import check_permission
-from lp.testing import (
- ANONYMOUS,
- login,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory
from lp.testing.layers import DatabaseFunctionalLayer
@@ -44,225 +26,3 @@
distro = self.factory.makeAccount(u'\u0170-account')
ignore, displayname, status_1, status_2 = repr(distro).rsplit(' ', 3)
self.assertEqual("'\\u0170-account'", displayname)
-
-
-class TestPersonlessAccountPermissions(TestCaseWithFactory):
- """In order for Person-less accounts to see their non-public details and
- email addresses, we had to change the security adapters for IAccount and
- IEmailAddress to accept the 'user' argument being either a Person or an
- Account.
-
- Here we login() with one of these person-less accounts and show that they
- can see their details, including email addresses.
- """
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- TestCaseWithFactory.setUp(self, 'no-priv@xxxxxxxxxxxxx')
- self.email = 'test@xxxxxxxxxxx'
- self.account = self.factory.makeAccount(
- 'Test account, without a person', email=self.email)
-
- def test_can_view_their_emails(self):
- login(self.email)
- self.failUnless(
- check_permission('launchpad.View', self.account.preferredemail))
-
- def test_can_view_their_own_details(self):
- login(self.email)
- self.failUnless(check_permission('launchpad.View', self.account))
-
- def test_can_change_their_own_details(self):
- login(self.email)
- self.failUnless(check_permission('launchpad.Edit', self.account))
-
- def test_emails_of_personless_acounts_cannot_be_seen_by_others(self):
- # Email addresses are visible to others only when the user has
- # explicitly chosen to have them shown, and that state is stored in
- # IPerson.hide_email_addresses, so for accounts that have no
- # associated Person, we will hide the email addresses from others.
- login('no-priv@xxxxxxxxxxxxx')
- self.failIf(check_permission(
- 'launchpad.View', self.account.preferredemail))
-
- # Anonymous users can't see them either.
- login(ANONYMOUS)
- self.failIf(check_permission(
- 'launchpad.View', self.account.preferredemail))
-
-
-class CreatePersonTests(TestCaseWithFactory):
- """Tests for `IAccount.createPerson`."""
-
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- super(CreatePersonTests, self).setUp(user='admin@xxxxxxxxxxxxx')
-
- def test_createPerson(self):
- account = self.factory.makeAccount("Test Account")
- # Account has no person.
- self.assertEqual(IPerson(account, None), None)
- self.assertEqual(account.preferredemail.person, None)
-
- person = account.createPerson(PersonCreationRationale.UNKNOWN)
- transaction.commit()
- self.assertNotEqual(person, None)
- self.assertEqual(person.account, account)
- self.assertEqual(IPerson(account), person)
- self.assertEqual(account.preferredemail.person, person)
-
- # Trying to create a person for an account with a person fails.
- self.assertRaises(AssertionError, account.createPerson,
- PersonCreationRationale.UNKNOWN)
-
- def test_createPerson_requires_email(self):
- # It isn't possible to create a person for an account with no
- # preferred email address.
- account = getUtility(IAccountSet).new(
- AccountCreationRationale.UNKNOWN, "Test Account")
- self.assertEqual(account.preferredemail, None)
- self.assertRaises(AssertionError, account.createPerson,
- PersonCreationRationale.UNKNOWN)
-
- def test_createPerson_sets_EmailAddress_person(self):
- # All email addresses for the account are associated with the
- # new person
- account = self.factory.makeAccount("Test Account")
- valid_email = self.factory.makeEmail(
- "validated@xxxxxxxxxxx", None, account,
- EmailAddressStatus.VALIDATED)
- new_email = self.factory.makeEmail(
- "new@xxxxxxxxxxx", None, account,
- EmailAddressStatus.NEW)
- old_email = self.factory.makeEmail(
- "old@xxxxxxxxxxx", None, account,
- EmailAddressStatus.OLD)
-
- person = account.createPerson(PersonCreationRationale.UNKNOWN)
- transaction.commit()
- self.assertEqual(valid_email.person, person)
- self.assertEqual(new_email.person, person)
- self.assertEqual(old_email.person, person)
-
- def test_createPerson_uses_name(self):
- # A optional user name can be provided. Normally the name is
- # generated from the email address.
- account = self.factory.makeAccount("Test Account")
- person = account.createPerson(
- PersonCreationRationale.UNKNOWN, name="sam.bell")
- self.failUnlessEqual("sam.bell", person.name)
-
- def test_createPerson_uses_comment(self):
- # An optional creation comment can be provided.
- account = self.factory.makeAccount("Test Account")
- person = account.createPerson(
- PersonCreationRationale.UNKNOWN,
- comment="when importing He-3 from the Moon")
- self.failUnlessEqual(
- "when importing He-3 from the Moon",
- person.creation_comment)
-
- def test_getByEmail_non_ascii_bytes(self):
- """Lookups for non-ascii addresses should raise LookupError.
-
- This tests the case where input is a bytestring.
- """
- with ExpectedException(LookupError, r"'SaraS\\xe1nchez@xxxxxxxxxxx'"):
- getUtility(IAccountSet).getByEmail('SaraS\xe1nchez@xxxxxxxxxxx')
-
- def test_getByEmail_non_ascii_unicode(self):
- """Lookups for non-ascii addresses should raise LookupError.
-
- This tests the case where input is a unicode string.
- """
- with ExpectedException(LookupError, r"u'SaraS\\xe1nchez@.*.net'"):
- getUtility(IAccountSet).getByEmail(u'SaraS\xe1nchez@xxxxxxxxxxx')
-
-
-class EmailManagementTests(TestCaseWithFactory):
- """Test email account management interfaces for `IAccount`."""
-
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- super(EmailManagementTests, self).setUp(user='admin@xxxxxxxxxxxxx')
-
- def test_setPreferredEmail(self):
- # Setting a new preferred email marks the old one as VALIDATED.
- account = self.factory.makeAccount("Test Account")
- first_email = account.preferredemail
- second_email = self.factory.makeEmail(
- "second-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.VALIDATED)
- transaction.commit()
- account.setPreferredEmail(second_email)
- transaction.commit()
- self.assertEqual(account.preferredemail, second_email)
- self.assertEqual(second_email.status, EmailAddressStatus.PREFERRED)
- self.assertEqual(first_email.status, EmailAddressStatus.VALIDATED)
-
- def test_setPreferredEmail_None(self):
- # Setting the preferred email to None sets the old preferred
- # email to VALIDATED.
- account = self.factory.makeAccount("Test Account")
- email = account.preferredemail
- transaction.commit()
- account.setPreferredEmail(None)
- transaction.commit()
- self.assertEqual(account.preferredemail, None)
- self.assertEqual(email.status, EmailAddressStatus.VALIDATED)
-
- def test_validateAndEnsurePreferredEmail(self):
- # validateAndEnsurePreferredEmail() sets the email status to
- # VALIDATED if there is no existing preferred email.
- account = self.factory.makeAccount("Test Account")
- self.assertNotEqual(account.preferredemail, None)
- new_email = self.factory.makeEmail(
- "new-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.NEW)
- transaction.commit()
- account.validateAndEnsurePreferredEmail(new_email)
- transaction.commit()
- self.assertEqual(new_email.status, EmailAddressStatus.VALIDATED)
-
- def test_validateAndEsnurePreferredEmail_no_preferred(self):
- # validateAndEnsurePreferredEmail() sets the new email as
- # preferred if there was no preferred email.
- account = self.factory.makeAccount("Test Account")
- account.setPreferredEmail(None)
- new_email = self.factory.makeEmail(
- "new-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.NEW)
- transaction.commit()
- account.validateAndEnsurePreferredEmail(new_email)
- transaction.commit()
- self.assertEqual(new_email.status, EmailAddressStatus.PREFERRED)
-
- def test_validated_emails(self):
- account = self.factory.makeAccount("Test Account")
- self.factory.makeEmail(
- "new-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.NEW)
- validated_email = self.factory.makeEmail(
- "validated-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.VALIDATED)
- self.factory.makeEmail(
- "old@xxxxxxxxxxx", None, account,
- EmailAddressStatus.OLD)
- transaction.commit()
- self.assertContentEqual(account.validated_emails, [validated_email])
-
- def test_guessed_emails(self):
- account = self.factory.makeAccount("Test Account")
- new_email = self.factory.makeEmail(
- "new-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.NEW)
- self.factory.makeEmail(
- "validated-email@xxxxxxxxxxx", None, account,
- EmailAddressStatus.VALIDATED)
- self.factory.makeEmail(
- "old@xxxxxxxxxxx", None, account,
- EmailAddressStatus.OLD)
- transaction.commit()
- self.assertContentEqual(account.guessed_emails, [new_email])
=== modified file 'lib/lp/services/mail/incoming.py'
--- lib/lp/services/mail/incoming.py 2011-12-30 02:24:09 +0000
+++ lib/lp/services/mail/incoming.py 2012-01-13 14:02:28 +0000
@@ -225,13 +225,7 @@
setupInteraction(authutil.unauthenticatedPrincipal())
return None
- # People with accounts but no related person will have a principal, but
- # the person adaptation will fail.
person = IPerson(principal, None)
- if person is None:
- setupInteraction(authutil.unauthenticatedPrincipal())
- return None
-
if person.account_status != AccountStatus.ACTIVE:
raise InactiveAccount(
"Mail from a user with an inactive account.")
=== modified file 'lib/lp/services/mail/tests/test_incoming.py'
--- lib/lp/services/mail/tests/test_incoming.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/mail/tests/test_incoming.py 2012-01-13 14:02:28 +0000
@@ -104,13 +104,6 @@
mail = self.factory.makeSignedMessage(email_address=unknown)
self.assertThat(authenticateEmail(mail), Is(None))
- def test_accounts_without_person(self):
- # An account without a person should be the same as an unknown email.
- email = 'non-person@xxxxxxxxxxx'
- self.factory.makeAccount(email=email)
- mail = self.factory.makeSignedMessage(email_address=email)
- self.assertThat(authenticateEmail(mail), Is(None))
-
class TestExtractAddresses(TestCaseWithFactory):
=== modified file 'lib/lp/services/verification/browser/logintoken.py'
--- lib/lp/services/verification/browser/logintoken.py 2012-01-05 00:15:32 +0000
+++ lib/lp/services/verification/browser/logintoken.py 2012-01-13 14:02:28 +0000
@@ -441,13 +441,11 @@
validated = (
EmailAddressStatus.VALIDATED, EmailAddressStatus.PREFERRED)
requester = self.context.requester
- account = requester.account
emailset = getUtility(IEmailAddressSet)
email = emailset.getByEmail(self.context.email)
if email is not None:
- if email.personID is not None and (
- requester is None or email.personID != requester.id):
+ if requester is None or email.personID != requester.id:
dupe = email.person
dname = cgi.escape(dupe.name)
# Yes, hardcoding an autogenerated field name is an evil
@@ -463,12 +461,6 @@
'case you should be able to <a href="${url}">merge them'
'</a> into a single one.',
mapping=dict(url=url))))
- elif account is not None and email.accountID != account.id:
- # Email address is owned by a personless account. We
- # can't offer to perform a merge here.
- self.addError(
- 'This email address is already registered for another '
- 'account')
elif email.status in validated:
self.addError(_(
"This email address is already registered and validated "
@@ -523,7 +515,7 @@
def markEmailAsValid(self, email):
"""Mark the given email address as valid."""
- self.context.requester.account.validateAndEnsurePreferredEmail(email)
+ self.context.requester.validateAndEnsurePreferredEmail(email)
class ValidateTeamEmailView(ValidateEmailView):
@@ -595,7 +587,6 @@
# that this new email does not have the PREFERRED status.
email.status = EmailAddressStatus.NEW
email.personID = requester.id
- email.accountID = requester.accountID
requester.validateAndEnsurePreferredEmail(email)
# Need to flush all changes we made, so subsequent queries we make
=== modified file 'lib/lp/services/webapp/authentication.py'
--- lib/lp/services/webapp/authentication.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/authentication.py 2012-01-13 14:02:28 +0000
@@ -68,7 +68,7 @@
if login is not None:
login_src = getUtility(IPlacelessLoginSource)
principal = login_src.getPrincipalByLogin(login)
- if principal is not None and principal.account.is_valid:
+ if principal is not None and principal.person.is_valid_person:
password = credentials.getPassword()
if principal.validate(password):
# We send a LoggedInEvent here, when the
@@ -107,7 +107,7 @@
# available in login source. This happens when account has
# become invalid for some reason, such as being merged.
return None
- elif principal.account.is_valid:
+ elif principal.person.is_valid_person:
login = authdata['login']
assert login, 'login is %s!' % repr(login)
notify(CookieAuthPrincipalIdentifiedEvent(
@@ -276,13 +276,11 @@
validate the password against so it may then email a validation
request to the user and inform them it has done so.
"""
- try:
- account = getUtility(IAccountSet).getByEmail(login)
- except LookupError:
+ person = getUtility(IPersonSet).getByEmail(login)
+ if person is None or person.account is None:
return None
- else:
- return self._principalForAccount(
- account, access_level, scope, want_password)
+ return self._principalForAccount(
+ person.account, access_level, scope, want_password)
def _principalForAccount(self, account, access_level, scope,
want_password=True):
=== modified file 'lib/lp/services/webapp/login.py'
--- lib/lp/services/webapp/login.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/login.py 2012-01-13 14:02:28 +0000
@@ -323,11 +323,11 @@
finally:
timeline_action.finish()
- def login(self, account):
+ def login(self, person):
loginsource = getUtility(IPlacelessLoginSource)
# We don't have a logged in principal, so we must remove the security
# proxy of the account's preferred email.
- email = removeSecurityProxy(account.preferredemail).email
+ email = removeSecurityProxy(person.preferredemail).email
logInPrincipal(
self.request, loginsource.getPrincipalByLogin(email), email)
@@ -383,7 +383,7 @@
return self.suspended_account_template()
with MasterDatabasePolicy():
- self.login(person.account)
+ self.login(person)
if should_update_last_write:
# This is a GET request but we changed the database, so update
=== modified file 'lib/lp/services/webapp/publication.py'
--- lib/lp/services/webapp/publication.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/publication.py 2012-01-13 14:02:28 +0000
@@ -341,9 +341,10 @@
# automated tests.
if request.get('PATH_INFO') not in [u'/+opstats', u'/+haproxy']:
principal = auth_utility.authenticate(request)
- if principal is None or principal.person is None:
- # This is either an unauthenticated user or a user who
- # authenticated on our OpenID server using a personless account.
+ if principal is not None:
+ assert principal.person is not None
+ else:
+ # This is an unauthenticated user.
principal = auth_utility.unauthenticatedPrincipal()
assert principal is not None, "Missing unauthenticated principal."
return principal
=== modified file 'lib/lp/services/webapp/tests/test_authentication.py'
--- lib/lp/services/webapp/tests/test_authentication.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_authentication.py 2012-01-13 14:02:28 +0000
@@ -9,17 +9,7 @@
import unittest
from contrib.oauth import OAuthRequest
-from zope.app.security.principalregistry import UnauthenticatedPrincipal
-
-from lp.services.config import config
-from lp.services.webapp.authentication import LaunchpadPrincipal
-from lp.services.webapp.login import logInPrincipal
-from lp.services.webapp.publication import LaunchpadBrowserPublication
-from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
- login,
- TestCaseWithFactory,
- )
+from lp.testing import TestCaseWithFactory
from lp.testing.layers import (
DatabaseFunctionalLayer,
LaunchpadFunctionalLayer,
@@ -31,32 +21,6 @@
)
-class TestAuthenticationOfPersonlessAccounts(TestCaseWithFactory):
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- TestCaseWithFactory.setUp(self)
- self.email = 'baz@xxxxxxxxxxx'
- self.request = LaunchpadTestRequest()
- self.account = self.factory.makeAccount(
- 'Personless account', email=self.email)
- self.principal = LaunchpadPrincipal(
- self.account.id, self.account.displayname,
- self.account.displayname, self.account)
- login(self.email)
-
- def test_navigate_anonymously_on_launchpad_dot_net(self):
- # A user with the credentials of a personless account will browse
- # launchpad.net anonymously.
- logInPrincipal(self.request, self.principal, self.email)
- self.request.response.setCookie(
- config.launchpad_session.cookie, 'xxx')
-
- publication = LaunchpadBrowserPublication(None)
- principal = publication.getPrincipal(self.request)
- self.failUnless(isinstance(principal, UnauthenticatedPrincipal))
-
-
class TestOAuthParsing(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/services/webapp/tests/test_authutility.py'
--- lib/lp/services/webapp/tests/test_authutility.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_authutility.py 2012-01-13 14:02:28 +0000
@@ -31,16 +31,16 @@
class DummyPerson(object):
implements(IPerson)
- is_valid = True
+ is_valid_person = True
class DummyAccount(object):
implements(IAccount)
- is_valid = True
person = DummyPerson()
Bruce = LaunchpadPrincipal(42, 'bruce', 'Bruce', DummyAccount(), 'bruce!')
+Bruce.person = Bruce.account.person
class DummyPlacelessLoginSource(object):
=== modified file 'lib/lp/services/webapp/tests/test_login.py'
--- lib/lp/services/webapp/tests/test_login.py 2012-01-04 11:57:57 +0000
+++ lib/lp/services/webapp/tests/test_login.py 2012-01-13 14:02:28 +0000
@@ -292,25 +292,6 @@
self.assertEquals(
view.fake_consumer.requested_url, 'http://example.com?foo=bar')
- def test_personless_account(self):
- # When there is no Person record associated with the account, we
- # create one.
- account = self.factory.makeAccount('Test account')
- self.assertIs(None, IPerson(account, None))
- with SRegResponse_fromSuccessResponse_stubbed():
- view, html = self._createViewWithResponse(account)
- self.assertIsNot(None, IPerson(account, None))
- self.assertTrue(view.login_called)
- response = view.request.response
- self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
- self.assertEquals(view.request.form['starting_url'],
- response.getHeader('Location'))
-
- # We also update the last_write flag in the session, to make sure
- # further requests use the master DB and thus see the newly created
- # stuff.
- self.assertLastWriteIsSet(view.request)
-
def test_unseen_identity(self):
# When we get a positive assertion about an identity URL we've never
# seen, we automatically register an account with that identity
@@ -329,11 +310,11 @@
account = account_set.getByOpenIDIdentifier(identifier)
self.assertIsNot(None, account)
self.assertEquals(AccountStatus.ACTIVE, account.status)
+ person = IPerson(account, None)
+ self.assertIsNot(None, person)
+ self.assertEquals('Foo User', person.displayname)
self.assertEquals('non-existent@xxxxxxxxxxx',
- removeSecurityProxy(account.preferredemail).email)
- person = IPerson(account, None)
- self.assertIsNot(None, person)
- self.assertEquals('Foo User', person.displayname)
+ removeSecurityProxy(person.preferredemail).email)
# We also update the last_write flag in the session, to make sure
# further requests use the master DB and thus see the newly created
=== modified file 'lib/lp/services/webapp/tests/test_login_account.py'
--- lib/lp/services/webapp/tests/test_login_account.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webapp/tests/test_login_account.py 2012-01-13 14:02:28 +0000
@@ -17,7 +17,6 @@
from lp.services.webapp.authentication import LaunchpadPrincipal
from lp.services.webapp.interfaces import (
CookieAuthLoggedInEvent,
- ILaunchpadPrincipal,
IPlacelessAuthUtility,
)
from lp.services.webapp.login import (
@@ -27,7 +26,6 @@
)
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.testing import (
- ANONYMOUS,
login,
TestCaseWithFactory,
)
@@ -172,34 +170,3 @@
principal = getUtility(IPlacelessAuthUtility).authenticate(
self.request)
self.failUnless(principal is None)
-
-
-class TestLoggingInWithPersonlessAccount(TestCaseWithFactory):
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- TestCaseWithFactory.setUp(self)
- self.request = LaunchpadTestRequest()
- login(ANONYMOUS)
- account_set = getUtility(IAccountSet)
- account, email = account_set.createAccountAndEmail(
- 'foo@xxxxxxxxxxx', AccountCreationRationale.UNKNOWN,
- 'Display name', 'password')
- self.principal = LaunchpadPrincipal(
- account.id, account.displayname, account.displayname, account)
- login('foo@xxxxxxxxxxx')
-
- def test_logInPrincipal(self):
- # logInPrincipal() will log the given principal in and not worry about
- # its lack of an associated Person.
- logInPrincipal(self.request, self.principal, 'foo@xxxxxxxxxxx')
-
- # Ensure we are using cookie auth.
- self.assertIsNotNone(
- self.request.response.getCookie(config.launchpad_session.cookie)
- )
-
- placeless_auth_utility = getUtility(IPlacelessAuthUtility)
- principal = placeless_auth_utility.authenticate(self.request)
- self.failUnless(ILaunchpadPrincipal.providedBy(principal))
- self.failUnless(principal.person is None)
=== modified file 'lib/lp/services/webservice/configuration.py'
--- lib/lp/services/webservice/configuration.py 2012-01-01 02:58:52 +0000
+++ lib/lp/services/webservice/configuration.py 2012-01-13 14:02:28 +0000
@@ -26,7 +26,7 @@
active_versions = ["beta", "1.0", "devel"]
last_version_with_mutator_named_operations = "beta"
first_version_with_total_size_link = "devel"
- view_permission = "launchpad.View"
+ view_permission = "launchpad.LimitedView"
require_explicit_versions = True
compensate_for_mod_compress_etag_modification = True
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-01-04 23:49:46 +0000
+++ lib/lp/testing/factory.py 2012-01-13 14:02:28 +0000
@@ -561,7 +561,7 @@
pocket)
return ProxyFactory(location)
- def makeAccount(self, displayname=None, email=None, password=None,
+ def makeAccount(self, displayname=None, password=None,
status=AccountStatus.ACTIVE,
rationale=AccountCreationRationale.UNKNOWN):
"""Create and return a new Account."""
@@ -570,13 +570,6 @@
account = getUtility(IAccountSet).new(
rationale, displayname, password=password)
removeSecurityProxy(account).status = status
- if email is None:
- email = self.getUniqueEmailAddress()
- email_status = EmailAddressStatus.PREFERRED
- if status != AccountStatus.ACTIVE:
- email_status = EmailAddressStatus.NEW
- email = self.makeEmail(
- email, person=None, account=account, email_status=email_status)
self.makeOpenIdIdentifier(account)
return account
@@ -731,10 +724,8 @@
# setPreferredEmail no longer activates the account
# automatically.
account = IMasterStore(Account).get(Account, person.accountID)
- account.activate(
- "Activated by factory.makePersonByName",
- password='foo',
- preferred_email=email)
+ account.reactivate(
+ "Activated by factory.makePersonByName", password='foo')
person.setPreferredEmail(email)
if not use_default_autosubscribe_policy:
@@ -745,20 +736,16 @@
MailingListAutoSubscribePolicy.NEVER)
account = IMasterStore(Account).get(Account, person.accountID)
getUtility(IEmailAddressSet).new(
- alternative_address, person, EmailAddressStatus.VALIDATED,
- account)
+ alternative_address, person, EmailAddressStatus.VALIDATED)
return person
- def makeEmail(self, address, person, account=None, email_status=None):
+ def makeEmail(self, address, person, email_status=None):
"""Create a new email address for a person.
:param address: The email address to create.
:type address: string
:param person: The person to assign the email address to.
:type person: `IPerson`
- :param account: The account to assign the email address to. Will use
- the given person's account if None is provided.
- :type person: `IAccount`
:param email_status: The default status of the email address,
if given. If not given, `EmailAddressStatus.VALIDATED`
will be used.
@@ -769,7 +756,7 @@
if email_status is None:
email_status = EmailAddressStatus.VALIDATED
return getUtility(IEmailAddressSet).new(
- address, person, email_status, account)
+ address, person, email_status)
def makeTeam(self, owner=None, displayname=None, email=None, name=None,
description=None, icon=None, logo=None,
=== modified file 'lib/lp/testing/tests/test_login.py'
--- lib/lp/testing/tests/test_login.py 2012-01-01 02:58:52 +0000
+++ lib/lp/testing/tests/test_login.py 2012-01-13 14:02:28 +0000
@@ -104,13 +104,6 @@
e = self.assertRaises(ValueError, login_person, team)
self.assertEqual(str(e), "Got team, expected person: %r" % (team,))
- def test_login_account(self):
- # Calling login_person with an account logs you in with that account.
- person = self.factory.makePerson()
- account = person.account
- login_person(account)
- self.assertLoggedIn(person)
-
def test_login_with_email(self):
# login() logs a person in by email.
email = 'test-email@xxxxxxxxxxx'
=== modified file 'lib/lp/testopenid/browser/server.py'
--- lib/lp/testopenid/browser/server.py 2012-01-01 02:58:52 +0000
+++ lib/lp/testopenid/browser/server.py 2012-01-13 14:02:28 +0000
@@ -230,9 +230,10 @@
else:
response = self.openid_request.answer(True)
+ person = IPerson(self.account)
sreg_fields = dict(
- nickname=IPerson(self.account).name,
- email=self.account.preferredemail.email,
+ nickname=person.name,
+ email=person.preferredemail.email,
fullname=self.account.displayname)
sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)
sreg_response = SRegResponse.extractResponse(
=== modified file 'lib/lp/translations/utilities/tests/test_file_importer.py'
--- lib/lp/translations/utilities/tests/test_file_importer.py 2011-12-30 01:48:17 +0000
+++ lib/lp/translations/utilities/tests/test_file_importer.py 2012-01-13 14:02:28 +0000
@@ -353,21 +353,6 @@
po_importer.potemplate.displayname),
'Did not create the correct comment for %s' % test_email)
- def test_getPersonByEmail_personless_account(self):
- # An Account without a Person attached is a difficult case for
- # _getPersonByEmail: it has to create the Person but re-use an
- # existing Account and email address.
- (pot_importer, po_importer) = self._createImporterForExportedEntries()
- test_email = 'freecdsplease@xxxxxxxxxxx'
- account = self.factory.makeAccount('Send me Ubuntu', test_email)
-
- person = po_importer._getPersonByEmail(test_email)
-
- self.assertEqual(account, person.account)
-
- # The same person will come up for the same address next time.
- self.assertEqual(person, po_importer._getPersonByEmail(test_email))
-
def test_getPersonByEmail_bad_address(self):
# _getPersonByEmail returns None for malformed addresses.
(pot_importer, po_importer) = self._createImporterForExportedEntries()
Follow ups