← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~benji/launchpad/better-HTML-generation into lp:launchpad

 

Benji York has proposed merging lp:~benji/launchpad/better-HTML-generation into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~benji/launchpad/better-HTML-generation/+merge/55960

This branch fixes the way structural subscription JavaScript constructs HTML, moving away from string concatenation to Y.Node.create() and friends.

There's also a fair bit of lint fixing required to quite JSLint.
-- 
https://code.launchpad.net/~benji/launchpad/better-HTML-generation/+merge/55960
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~benji/launchpad/better-HTML-generation into lp:launchpad.
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js	2011-03-30 15:23:42 +0000
+++ lib/lp/registry/javascript/structural-subscription.js	2011-04-01 16:08:35 +0000
@@ -11,9 +11,6 @@
 
 var namespace = Y.namespace('lp.registry.structural_subscription');
 
-var INNER_HTML = 'innerHTML',
-    VALUE = 'value';
-
 var FILTER_COMMENTS = 'filter-comments',
     FILTER_WRAPPER = 'filter-wrapper',
     ACCORDION_WRAPPER = 'accordion-wrapper',
@@ -21,8 +18,7 @@
     ADDED_OR_CHANGED = 'added-or-changed',
     ADVANCED_FILTER = 'advanced-filter',
     MATCH_ALL = 'match-all',
-    MATCH_ANY = 'match-any',
-    SS_COLLAPSIBLE = 'ss-collapsible'
+    MATCH_ANY = 'match-any'
     ;
 
 var add_subscription_overlay;
@@ -61,7 +57,7 @@
 function list_contains(list, target) {
     // The list may be undefined in some cases.
     return Y.Lang.isArray(list) && list.indexOf(target) !== -1;
-};
+}
 
 // Expose to tests.
 namespace._list_contains = list_contains;
@@ -209,8 +205,9 @@
 function save_subscription(form_data) {
     var who;
     var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
-    if (has_errors)
+    if (has_errors) {
         return false;
+    }
     if (form_data.recipient[0] === 'user') {
         who = LP.links.me;
     } else {
@@ -223,15 +220,18 @@
 
 function check_for_errors_in_overlay(overlay) {
     var has_errors = false;
-    var errors = new Array();
-    for (var field in overlay.field_errors) {
-        if (overlay.field_errors[field]) {
-            has_errors = true;
-            errors.push(field);
+    var errors = [];
+    var field;
+    for (field in overlay.field_errors) {
+        if (overlay.field_errors.hasOwnProperty(field)) {
+            if (overlay.field_errors[field]) {
+                has_errors = true;
+                errors.push(field);
+            }
         }
-    };
+    }
     if (has_errors) {
-        var error_text = errors.pop()
+        var error_text = errors.pop();
         if (errors.length > 0) {
             error_text = errors.join(', ') + ' and ' + error_text;
         }
@@ -242,40 +242,47 @@
     } else {
         return false;
     }
-};
+}
 
 /**
  * Handle the activation of the edit subscription link.
  */
 function edit_subscription_handler(context, form_data) {
     var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
-    if (has_errors)
+    var filter_id = '#filter-description-'+context.filter_id.toString();
+    if (has_errors) {
         return false;
+    }
     var on = {success: function (new_data) {
         var filter = new_data.getAttrs();
-        var description_node = Y.one(
-            '#filter-description-'+context.filter_id.toString());
-        description_node.set(
-            INNER_HTML, render_filter_description(filter));
-        var name_node = Y.one(
-            '#filter-name-'+context.filter_id.toString());
-        name_node.set(
-            INNER_HTML, render_filter_name(context.filter_info, filter));
+        var description_node = Y.one(filter_id);
+        description_node
+            .empty()
+            .appendChild(create_filter_description(filter));
+        description_node.ancestor('.subscription-filter').one('.filter-name')
+            .empty()
+            .appendChild(render_filter_title(context.filter_info, filter));
         add_subscription_overlay.hide();
     }};
     patch_bug_filter(context.filter_info.filter, form_data, on);
 }
 
+/**
+ * Ititilize the overlay errors and set up field validators.
+ */
 function setup_overlay_validators(overlay, overlay_id) {
+<<<<<<< TREE
     overlay.field_validators = {
         tags: get_error_for_tags_list
     };
+=======
+>>>>>>> MERGE-SOURCE
     overlay.field_errors = {};
-    for (var field_name in overlay.field_validators) {
-        add_input_validator(overlay, overlay_id, field_name,
-                            overlay.field_validators[field_name]);
-    };
-};
+    add_input_validator(overlay, overlay_id,
+        'name', get_error_for_subscription_name);
+    add_input_validator(overlay, overlay_id,
+        'tags', get_error_for_tags_list);
+}
 
 /**
  * Populate the overlay element with the contents of the add/edit form.
@@ -295,8 +302,9 @@
         form_submit_callback: function(formdata) {
             // Do not clean up if saving was not successful.
             var save_succeeded = submit_callback(formdata);
-            if (save_succeeded !== false)
+            if (save_succeeded !== false) {
                 clean_up();
+            }
         }
     });
     add_subscription_overlay.render(content_box_id);
@@ -373,10 +381,14 @@
  * @param {String} name Name of the control.
  */
 function make_cell(item, name) {
-    return '<td style="padding-left:3px"><label><input type="checkbox" ' +
-        'name="' + name +'" ' +
-        'value="' + item + '" checked="checked">' +
-        item + '</label><td>';
+    var cell = Y.Node.create('<td style="padding-left:3px"><td>');
+    cell.appendChild('<label></label>')
+        .append(Y.Node.create('<input type="checkbox" checked></input>')
+            .set('name', name)
+            .set('value', item))
+        .append(Y.Node.create('<span></span>')
+            .set('text', item));
+    return cell;
 }
 /**
  * Make a table.
@@ -388,19 +400,15 @@
  * @param {Int} num_cols The number of columns for the table to use.
  */
 function make_table(list, name, num_cols) {
-    var html = '<table>';
-    var i;
+    var table = Y.Node.create('<table></table>');
+    var i, row;
     for (i=0; i<list.length; i++) {
         if (i % num_cols === 0) {
-            if (i !== 0) {
-                html += '</tr>';
-            }
-            html += '<tr>';
+            row = table.appendChild('<tr></tr>');
         }
-        html += make_cell(list[i], name);
+        row.appendChild(make_cell(list[i], name));
     }
-    html += '</tr></table>';
-    return html;
+    return table;
 }
 
 /**
@@ -413,16 +421,18 @@
  * @return {Object} Hash with 'all_name', 'none_name', and 'html' keys.
  */
 function make_selector_controls(parent) {
+    var selectors_id = parent + '-selectors';
     var rv = {};
-    rv.all_name = parent + '-select-all';
-    rv.none_name = parent + '-select-none';
-    rv.html = '<div id="'+ parent + '-selectors" '+
-                 'style="margin-left: 10px;margin-bottom: 10px">' +
-                 '  <a href="#" id="' + rv.all_name +
-                 '">Select all</a> &nbsp;' +
-                 '  <a href="#" id="' + rv.none_name +
-                 '">Select none</a>' +
-                 '</div>';
+    rv.all_link = Y.Node.create(
+        '<a href="#" class="select-all">Select all</a>');
+    rv.none_link = Y.Node.create(
+        '<a href="#" class="select-none">Select none</a>');
+    rv.node = Y.Node.create(
+            '<div style="margin-left: 10px;margin-bottom: 10px"></div>')
+        .set('id', selectors_id)
+        .append(rv.all_link)
+        .append(' &nbsp; ')
+        .append(rv.none_link);
 
     return rv;
 }
@@ -474,27 +484,23 @@
         id: "tags_ai",
         contentHeight: {method: "auto"}
     } );
-
     tags_container = tags_ai;
-
-    tags_ai.set("bodyContent",
-        '<div>\n' +
-        '<div>\n' +
-        '    <input type="radio" name="tag_match" value="' +
-                   MATCH_ALL + '" checked> Match all tags\n' +
-        '    <input type="radio" name="tag_match" value="' +
-                   MATCH_ANY + '"> Match any tags\n' +
-        '</div>\n' +
-        '<div style="padding-bottom:10px;">\n' +
-        '    <input type="text" name="tags" size="60"/>\n' +
+    tags_ai.set("bodyContent", Y.Node.create('<div><div></div></div>')
+        .append(Y.Node.create('<input type="radio" name="tag_match" checked>')
+            .set('value', MATCH_ALL))
+        .append('Match all tags')
+        .append(Y.Node.create('<input type="radio" name="tag_match" checked>')
+            .set('value', MATCH_ANY))
+        .append('Match any tags')
+        .append(
+        '<div style="padding-bottom:10px;">' +
+        '    <input type="text" name="tags" size="60"/>' +
         '    <a target="help"'+
         '        href="/+help/structural-subscription-tags.html" ' +
         '        class="sprite maybe">&nbsp;'+
                '<span class="invisible-link">Structural subscription tags '+
-        '       help</span></a>\n ' +
-        '</div>\n' +
-        '</div>\n');
-
+        '       help</span></a> ' +
+        '</div>'));
     accordion.addItem(tags_ai);
 
     // Build importances pane.
@@ -507,20 +513,17 @@
     } );
     var importances = LP.cache.importances;
     var selectors = make_selector_controls('importances');
-    var importances_html = '<div id="importances-wrapper">' +
-                           selectors.html +
-                           make_table(importances, 'importances', 4) +
-                           '</div>';
-    importances_ai.set("bodyContent", importances_html);
+    importances_ai.set("bodyContent",
+        Y.Node.create('<div id="importances-wrapper"></div>')
+            .append(selectors.node)
+            .append(make_table(importances, 'importances', 4)));
     accordion.addItem(importances_ai);
     // Wire up the 'all' and 'none' selectors.
-    var all_link = content_node.one('#' + selectors.all_name);
-    var none_link = Y.one('#' + selectors.none_name);
     var node = content_node.one('#importances-wrapper');
-    var select_all_handler = make_select_handler(node, importances, true);
-    var select_none_handler = make_select_handler(node, importances, false);
-    all_link.on('click', select_all_handler);
-    none_link.on('click', select_none_handler);
+    selectors.all_link.on('click',
+        make_select_handler(node, importances, true));
+    selectors.none_link.on('click',
+        make_select_handler(node, importances, false));
 
     // Build statuses pane.
     statuses_ai = new Y.AccordionItem( {
@@ -532,18 +535,17 @@
     } );
     var statuses = LP.cache.statuses;
     selectors = make_selector_controls('statuses');
-    var status_html = '<div id="statuses-wrapper">' +
-                      selectors.html + make_table(statuses, 'statuses', 3)+
-                      '</div>';
-    statuses_ai.set("bodyContent", status_html);
+    statuses_ai.set("bodyContent",
+        Y.Node.create('<div id="statuses-wrapper"></div>')
+            .append(selectors.node)
+            .append(make_table(statuses, 'statuses', 3)));
     accordion.addItem(statuses_ai);
-    all_link = content_node.one('#' + selectors.all_name);
-    none_link = Y.one('#' + selectors.none_name);
+    // Wire up the 'all' and 'none' selectors.
     node = content_node.one('#statuses-wrapper');
-    select_all_handler = make_select_handler(node, statuses, true);
-    select_none_handler = make_select_handler(node, statuses, false);
-    all_link.on('click', select_all_handler);
-    none_link.on('click', select_none_handler);
+    selectors.all_link.on('click',
+        make_select_handler(node, statuses, true));
+    selectors.none_link.on('click',
+        make_select_handler(node, statuses, false));
 
     return accordion;
 }
@@ -624,105 +626,125 @@
 }
 
 /**
+ * Add a recipient picker to the overlay.
+ */
+function add_recipient_picker(content_box, hide) {
+    var no_recipient_picker = Y.Node.create(
+        '<input type="hidden" name="recipient" value="user">' +
+        '<span>Yourself</span>');
+    var recipient_picker = Y.Node.create(
+        '<label><input type="radio" name="recipient" value="user" checked>' +
+        '  Yourself</label><br>' +
+        '<label><input type="radio" name="recipient" value="team">' +
+        '  One of the teams you administer</label><br>' +
+        '<dl style="margin-left:25px;">' +
+        '  <dt></dt>' +
+        '  <dd>' +
+        '    <select name="team" id="structural-subscription-teams">' +
+        '    </select>' +
+        '  </dd>' +
+        '</dl>');
+    var teams = LP.cache.administratedTeams;
+    var node = content_box.one('#bug-mail-recipient');
+    node.empty();
+    // Populate the team drop down from LP.cache data, if appropriate.
+    if (!hide && teams.length > 0) {
+        var select = recipient_picker.one('#structural-subscription-teams');
+        var i;
+        for (i=0; i<teams.length; i++) {
+            select.append(Y.Node.create('<option></option>')
+                .set('text', teams[i].title)
+                .set('value', teams[i].link));
+        }
+        select.on(
+            'focus',
+            function () {
+                Y.one('input[value="team"][name="recipient"]').set(
+                    'checked', true);
+            }
+        );
+        node.append(recipient_picker);
+    } else {
+        node.append(no_recipient_picker);
+    }
+}
+
+/**
  * Construct the overlay and populate it with the add/edit form.
  */
 function setup_overlay(content_box_id, hide_recipient_picker) {
     var content_node = Y.one(content_box_id);
-    var container = Y.Node.create('<div id="overlay-container"></div>');
-    var accordion_overlay_id = 'accordion-overlay';
-    var teams = LP.cache.administratedTeams;
-    var no_recipient_picker =
-        '      <input type="hidden" name="recipient" value="user">\n' +
-        '      <span>Yourself</span>\n',
-        recipient_picker =
-        '      <input type="radio" name="recipient" value="user"\n'+
-        '            id="structural-subscription-recipient-user" checked>\n'+
-        '      <label for="structural-subscription-recipient-user">\n'+
-        '        Yourself</label><br>\n' +
-        '      <input type="radio" name="recipient"\n'+
-        '            id="structural-subscription-recipient-team"\n'+
-        '            value="team">\n'+
-        '      <label for="structural-subscription-recipient-team">One of\n'+
-        '        the teams you administer</label><br>\n' +
-        '      <dl style="margin-left:25px;">\n' +
-        '        <dt></dt>\n' +
-        '        <dd>\n' +
-        '          <select name="team" id="structural-subscription-teams">\n'+
-        '          </select>\n' +
-        '        </dd>\n' +
-        '      </dl>\n',
-        control_code =
-        '<dl>\n' +
-        '    <dt>Bug mail recipient</dt>\n' +
-        '    <dd>\n' +
-        ((!hide_recipient_picker && teams.length > 0) ?
-         recipient_picker : no_recipient_picker) +
-        '    </dd>\n' +
-        '  <dt>Subscription name</dt>\n' +
-        '  <dd>\n' +
-        '    <input type="text" name="name">\n' +
-        '    <a target="help" class="sprite maybe"\n' +
-        '          href="/+help/structural-subscription-name.html">&nbsp;\n' +
-        '      <span class="invisible-link">Structural subscription\n'+
-        '        description help</span></a>\n ' +
-        '  </dd>\n' +
-        '  <dt>Receive mail for bugs affecting\n'+
-        '    <span id="structural-subscription-context-title">\n'+
-        '      '+LP.cache.context.title+'</span> that</dt>\n' +
-        '  <dd>\n' +
-        '    <div id="events">\n' +
-        '      <input type="radio" name="events"\n' +
-        '          value="' + ADDED_OR_CLOSED + '"\n'+
-        '          id="' + ADDED_OR_CLOSED + '" checked>\n'+
-        '      <label for="'+ADDED_OR_CLOSED+'">are added or '+
-        '        closed</label>\n'+
-        '      <br>\n' +
-        '      <input type="radio" name="events"\n'+
-        '          value="' + ADDED_OR_CHANGED + '"\n' +
-        '          id="' + ADDED_OR_CHANGED + '">\n'+
-        '      <label for="'+ADDED_OR_CHANGED+'">are added or changed in\n'+
-        '        any way\n'+
-        '        <em id="'+ADDED_OR_CHANGED+'-more">(more options...)</em>\n'+
-        '      </label>\n' +
-        '    </div>\n' +
-        '    <div id="' + FILTER_WRAPPER + '" class="ss-collapsible">\n' +
-        '    <dl style="margin-left:25px;">\n' +
-        '      <dt></dt>\n' +
-        '      <dd>\n' +
-        '        <input type="checkbox" name="filters"\n' +
-        '            value="' + FILTER_COMMENTS + '"\n'+
-        '            id="'+FILTER_COMMENTS+'">\n' +
-        '        <label for="'+FILTER_COMMENTS+'">Don\'t send mail about\n'+
-        '          comments</label><br>\n' +
-        '        <input type="checkbox" name="filters"\n' +
-        '            value="' + ADVANCED_FILTER + '"\n' +
-        '            id="' + ADVANCED_FILTER + '">\n' +
-        '        <label for="'+ADVANCED_FILTER+'">Bugs must match this\n'+
-        '          filter <em id="'+ADVANCED_FILTER+'-more">(...)</em>\n'+
-        '        </label><br>\n' +
-        '        <div id="' + ACCORDION_WRAPPER + '" \n' +
-        '            class="' + SS_COLLAPSIBLE + '">\n' +
-        '            <dl>\n' +
-        '                <dt></dt>\n' +
-        '                <dd style="margin-left:25px;">\n' +
-        '                    <div id="' + accordion_overlay_id + '"\n' +
-        '                        style="position:relative; '+
-                                       'overflow:hidden;"></div>\n' +
-        '                </dd>\n' +
-        '            </dl>\n' +
-        '        </div> \n' +
-        '      </dd>\n' +
-        '    </dl>\n' +
-        '    </div> \n' +
-        '  </dd>\n' +
-        '  <dt></dt>\n' +
-        '</dl>';
-
-    content_node.appendChild(container);
-    container.appendChild(Y.Node.create(control_code));
-
-    var accordion = create_accordion(
-        '#' + accordion_overlay_id, content_node);
+    var container = Y.Node.create(
+        '<div id="overlay-container"><dl>' +
+        '    <dt>Bug mail recipient</dt>' +
+        '    <dd id="bug-mail-recipient">' +
+        '    </dd>' +
+        '  <dt>Subscription name</dt>' +
+        '  <dd>' +
+        '    <input type="text" name="name">' +
+        '    <a target="help" class="sprite maybe"' +
+        '          href="/+help/structural-subscription-name.html">&nbsp;' +
+        '      <span class="invisible-link">Structural subscription' +
+        '        description help</span></a> ' +
+        '  </dd>' +
+        '  <dt>Receive mail for bugs affecting' +
+        '    <span id="structural-subscription-context-title"></span> that</dt>' +
+        '  <dd>' +
+        '    <div id="events">' +
+        '      <input type="radio" name="events"' +
+        '          value="added-or-closed"' +
+        '          id="added-or-closed" checked>' +
+        '      <label for="added-or-closed">are added or ' +
+        '        closed</label>' +
+        '      <br>' +
+        '      <input type="radio" name="events"' +
+        '          value="added-or-changed"' +
+        '          id="added-or-changed">' +
+        '      <label for="added-or-changed">are added or changed in' +
+        '        any way' +
+        '        <em id="added-or-changed-more">(more options...)</em>' +
+        '      </label>' +
+        '    </div>' +
+        '    <div id="filter-wrapper" class="ss-collapsible">' +
+        '    <dl style="margin-left:25px;">' +
+        '      <dt></dt>' +
+        '      <dd>' +
+        '        <input type="checkbox" name="filters"' +
+        '            value="filter-comments"' +
+        '            id="filter-comments">' +
+        '        <label for="filter-comments">Don\'t send mail about' +
+        '          comments</label><br>' +
+        '        <input type="checkbox" name="filters"' +
+        '            value="advanced-filter"' +
+        '            id="advanced-filter">' +
+        '        <label for="advanced-filter">Bugs must match this' +
+        '          filter <em id="advanced-filter-more">(...)</em>' +
+        '        </label><br>' +
+        '        <div id="accordion-wrapper" ' +
+        '            class="ss-collapsible">' +
+        '            <dl>' +
+        '                <dt></dt>' +
+        '                <dd style="margin-left:25px;">' +
+        '                    <div id="accordion-overlay"' +
+        '                        style="position:relative; overflow:hidden;"></div>' +
+        '                </dd>' +
+        '            </dl>' +
+        '        </div> ' +
+        '      </dd>' +
+        '    </dl>' +
+        '    </div> ' +
+        '  </dd>' +
+        '  <dt></dt>' +
+        '</dl></div>');
+
+    // Assemble some nodes and set the title.
+    content_node
+        .appendChild(container)
+            .one('#structural-subscription-context-title')
+                .set('text', LP.cache.context.title);
+
+    var accordion = create_accordion('#accordion-overlay', content_node);
+    add_recipient_picker(container, hide_recipient_picker);
 
     // Set up click handlers for the events radio buttons.
     var radio_group = Y.all('#events input');
@@ -735,26 +757,6 @@
     advanced_filter.on(
         'change',
         function() {handle_change(ADVANCED_FILTER, ACCORDION_WRAPPER);});
-    // Populate the team drop down from LP.cache data, if appropriate.
-    if (!hide_recipient_picker && teams.length > 0) {
-        var select = Y.one('#structural-subscription-teams');
-        var i;
-        var team;
-        for (i=0; i<teams.length; i++) {
-            team = teams[i];
-            var option = Y.Node.create('<option></option>');
-            option.set(INNER_HTML, team.title);
-            option.set(VALUE, team.link);
-            select.appendChild(option);
-        }
-        select.on(
-            'focus',
-            function () {
-                Y.one('input[value="team"][name="recipient"]').set(
-                    'checked', true);
-            }
-        );
-    }
     return '#' + container._node.id;
 }                               // setup_overlay
 // Expose in the namespace for testing purposes.
@@ -807,7 +809,7 @@
         var overlay_id = setup_overlay(config.content_box, true);
         var submit_button = Y.Node.create(
             '<button type="submit" name="field.actions.create" ' +
-            'value="Save Changes" class="lazr-pos lazr-btn" '+
+            'value="Save Changes" class="lazr-pos lazr-btn" ' +
             '>OK</button>');
         // This is a bit of an odd approach, but it lets us retrofit code
         // without a large refactoring.  When edit_subscription_handler is
@@ -848,8 +850,9 @@
             overlay.field_errors[field_name] = true;
             // Accordion sets fixed height for the accordion item,
             // so we have to resize the tags container.
-            if (field_name == 'tags')
+            if (field_name === 'tags') {
                 tags_container.resize();
+            }
             // Firefox prohibits focus from inside the 'focus lost' event
             // handler (probably to stop loops), so we need to run
             // it from a different context (which we do with setTimeout).
@@ -859,8 +862,23 @@
             overlay.field_errors[field_name] = false;
         }
     });
-};
-
+}
+
+<<<<<<< TREE
+=======
+function get_error_for_subscription_name(value) {
+    if (value.search(/[<>"&]/) === -1) {
+        return null;
+    } else {
+        return ('Subscription name cannot contain any of the following ' +
+                'characters: &lt; &gt; &quot; &amp;');
+    }
+}
+
+// Export for testing
+namespace._get_error_for_subscription_name = get_error_for_subscription_name;
+
+>>>>>>> MERGE-SOURCE
 function get_error_for_tags_list(value) {
     // See database/schema/trusted.sql valid_name() function
     // which is used to validate a single tag.
@@ -874,7 +892,8 @@
                 'digits 0-9 and symbols "+", "-" or ".", and they ' +
                 'must start with a lowercase letter or a digit.');
     }
-};
+}
+
 // Export for testing
 namespace._get_error_for_tags_list = get_error_for_tags_list;
 
@@ -956,12 +975,12 @@
                 var i;
                 for (i=0; i<teams.length; i++) {
                     if (teams[i].link === filter_info.subscriber_link){
-                        recipient_label.set(INNER_HTML, teams[i].title);
+                        recipient_label.set('text', teams[i].title);
                         break;
                     }
                 }
             } else {
-                recipient_label.set(INNER_HTML, 'Yourself');
+                recipient_label.set('text', 'Yourself');
             }
             content_node.one('[name="name"]').set('value',filter.description);
             if (is_lifecycle) {
@@ -1011,10 +1030,10 @@
             context.filter_info = filter_info;
             context.filter_id = filter_id;
             var title = subscription.target_title;
-            Y.one('#structural-subscription-context-title').set(
-                INNER_HTML, title);
-            Y.one('#subscription-overlay-title').set(
-                INNER_HTML, 'Edit subscription for '+title+' bugs');
+            Y.one('#structural-subscription-context-title')
+                .set('text', title);
+            Y.one('#subscription-overlay-title')
+                .set('text', 'Edit subscription for '+title+' bugs');
             add_subscription_overlay.show();
         }
     };
@@ -1035,14 +1054,14 @@
             headers: {'X-HTTP-Method-Override': 'DELETE'},
             on: {success: function(transactionid, response, args){
                     var subscriber = Y.one(
-                        '#subscription-'+subscriber_id.toString()),
-                        node = subscriber,
-                        filters = subscriber.all('.subscription-filter');
+                        '#subscription-'+subscriber_id.toString());
+                    var to_collapse = subscriber;
+                    var filters = subscriber.all('.subscription-filter');
                     if (!filters.isEmpty()) {
-                        node = Y.one(
+                        to_collapse = Y.one(
                             '#subscription-filter-'+filter_id.toString());
                         }
-                    collapse_node(node);
+                    collapse_node(to_collapse);
                     },
                  failure: error_handler.getFailureHandler()
                 }
@@ -1066,11 +1085,12 @@
             var filter_info = sub.filters[j];
             if (!filter_info.subscriber_is_team ||
                 filter_info.user_is_team_admin) {
-                var edit_link = Y.one('#edit-'+filter_id.toString());
+                var node = Y.one('#subscription-filter-'+filter_id.toString());
+                var edit_link = node.one('a.edit-subscription');
                 var edit_handler = make_edit_handler(
                     sub, filter_info, filter_id, config, context);
                 edit_link.on('click', edit_handler);
-                var delete_link = Y.one('#unsubscribe-'+filter_id.toString());
+                var delete_link = node.one('a.delete-subscription');
                 var delete_handler = make_delete_handler(
                     filter_info.filter, filter_id, i);
                 delete_link.on('click', delete_handler);
@@ -1085,22 +1105,27 @@
  */
 function fill_in_bug_subscriptions(config, context) {
     validate_config(config);
+
     var listing = Y.one('#subscription-listing');
     var subscription_info = LP.cache.subscription_info;
-    var html = '<div class="yui-g"><div id="structural-subscriptions">';
+    var top_node = Y.Node.create(
+        '<div class="yui-g"><div id="structural-subscriptions"></div></div>');
     var filter_id = 0;
     var i;
     var j;
     for (i=0; i<subscription_info.length; i++) {
         var sub = subscription_info[i];
-        html +=
+        var sub_node = top_node.appendChild(Y.Node.create(
             '<div style="margin-top: 2em; padding: 0 1em 1em 1em; '+
-            '      border: 1px solid #ddd;"'+
-            '      id="subscription-'+i.toString()+'">'+
-            '  <span style="float: left; margin-top: -0.6em; padding: 0 1ex;'+
-            '      background-color: #fff;">Subscriptions to'+
-            '    <a href="'+sub.target_url+'">'+sub.target_title+'</a>'+
-            '  </span>';
+            '      border: 1px solid #ddd;"></div>')
+            .set('id', 'subscription-'+i.toString()));
+        sub_node.appendChild(Y.Node.create(
+            '  <span style="float: left; margin-top: -0.6em; '+
+            '      padding: 0 1ex; background-color: #fff;"></a>'))
+            .appendChild('<span>Subscriptions to </span>')
+                .appendChild(Y.Node.create('<a></a>')
+                    .set('href', sub.target_url)
+                    .set('text', sub.target_title));
 
         for (j=0; j<sub.filters.length; j++) {
             var filter = sub.filters[j].filter;
@@ -1110,52 +1135,53 @@
             // and see the information you expect to see.
             LP.cache['structural-subscription-filter-'+filter_id.toString()] =
                 filter;
-            html +=
+            var filter_node = sub_node.appendChild(Y.Node.create(
                 '<div style="margin: 1em 0em 0em 1em"'+
-                '      id="subscription-filter-'+filter_id.toString()+'"'+
-                '      class="subscription-filter">'+
-                '  <div style="margin-top: 1em">'+
-                '    <strong id="filter-name-'+
-                filter_id.toString()+'">'+
-                render_filter_name(sub.filters[j], filter)+'</strong>';
+                '      class="subscription-filter"></div>')
+                .set('id', 'subscription-filter-'+filter_id.toString()))
+                .appendChild(Y.Node.create(
+                    '<div style="margin-top: 1em"></div>'));
+            filter_node.appendChild(Y.Node.create(
+                '<strong class="filter-name"></strong>'))
+                .appendChild(render_filter_title(sub.filters[j], filter));
+
             if (!sub.filters[j].subscriber_is_team ||
                 sub.filters[j].user_is_team_admin) {
                 // User can edit the subscription.
-                html +=
+                filter_node.appendChild(Y.Node.create(
                     '<span style="float: right">'+
-                    '<a href="#" class="sprite modify edit js-action"'+
-                    '    id="edit-'+filter_id.toString()+'">'+
+                    '<a href="#" class="sprite modify edit js-action '+
+                    '    edit-subscription">'+
                     '  Edit this subscription</a> or '+
-                    '<a href="#" class="sprite modify remove js-action"'+
-                    '    id="unsubscribe-'+filter_id.toString()+'">'+
-                    '  Unsubscribe</a></span>';
+                    '<a href="#" class="sprite modify remove js-action '+
+                    '    delete-subscription">'+
+                    '  Unsubscribe</a></span>'));
             } else {
                 // User cannot edit the subscription, because this is a
                 // team and the user does not have admin privileges.
-                html +=
+                filter_node.appendChild(Y.Node.create(
                     '<span style="float: right"><em>'+
                     'You do not have privileges to change this subscription'+
-                    '</em></span>';
+                    '</em></span>'));
             }
-            html += '</div>';
-            html +=
-                '<div style="padding-left: 1em"'+
-                '      id="filter-description-'+filter_id.toString()+'">'+
-                render_filter_description(filter)+'</div>';
-
-            html += '</div>';
+
+            filter_node.appendChild(Y.Node.create(
+                '<div style="padding-left: 1em"></div>')
+                .set('id', 'filter-description-'+filter_id.toString()))
+                .appendChild(create_filter_description(filter));
+
             filter_id += 1;
         }
 
         // We can remove this once we enforce at least one filter per
         // subscription.
         if (subscription_info[i].filters.length === 0) {
-            html += '<strong>All messages</strong>';
+            sub_node.appendChild(
+                '<div style="clear: both; padding: 1em 0 0 1em"></div>')
+                .appendChild('<strong>All messages</strong>');
         }
-        html += '</div>';
     }
-    html += '</div></div>';
-    listing.appendChild(Y.Node.create(html));
+    listing.appendChild(top_node);
 
     wire_up_edit_links(config, context);
 }
@@ -1163,7 +1189,8 @@
 /**
  * Construct a one-line textual description of a filter's name.
  */
-function render_filter_name(filter_info, filter) {
+function render_filter_title(filter_info, filter) {
+    var title = Y.Node.create('<span></span>');
     var description;
     if (filter.description) {
         description = '"'+filter.description+'"';
@@ -1171,62 +1198,79 @@
         description = '(unnamed)';
     }
     if (filter_info.subscriber_is_team) {
-        return '<a href="'+filter_info.subscriber_url+'">'+
-             filter_info.subscriber_title+"</a> subscription: "+description;
+        title.appendChild(Y.Node.create('<a></a>'))
+            .set('href', filter_info.subscriber_url)
+            .set('text', filter_info.subscriber_title);
+        title.appendChild(Y.Node.create('<span></span>'))
+             .set('text', ' subscription: '+description);
     } else {
-        return 'Your subscription: '+description;
+        title.set('text', 'Your subscription: '+description);
     }
+    return title;
 }
 
 /**
  * Construct a textual description of all of filter's properties.
  */
-function render_filter_description(filter) {
-    var html = '';
-    var filter_items = '';
+function create_filter_description(filter) {
+    var description = Y.Node.create('<div></div>');
+
+    var filter_items = [];
     // Format status conditions.
     if (filter.statuses.length !== 0) {
-        filter_items += '<li> have status ' +
-            filter.statuses.join(', ');
+        filter_items.push(Y.Node.create('<li></li>')
+            .set('text', 'have status '+filter.statuses.join(', ')));
     }
 
     // Format importance conditions.
     if (filter.importances.length !== 0) {
-        filter_items += '<li> are of importance ' +
-            filter.importances.join(', ');
+        filter_items.push(Y.Node.create('<li></li>')
+            .set('text', 'are of importance '+filter.importances.join(', ')));
     }
 
     // Format tag conditions.
     if (filter.tags.length !== 0) {
-        filter_items += '<li> are tagged with ';
+        var tag_desc = Y.Node.create('<li>are tagged with </li>')
+            .append(Y.Node.create('<strong></strong>'))
+            .append(Y.Node.create('<span> of these tags: </span>'))
+            .append(Y.Node.create('<span></span>')
+                .set('text', filter.tags.join(', ')));
+
         if (filter.find_all_tags) {
-            filter_items += '<strong>all</strong>';
+            tag_desc.one('strong').set('text', 'all');
         } else {
-            filter_items += '<strong>any</strong>';
+            tag_desc.one('strong').set('text', 'any');
         }
-        filter_items += ' of these tags: ' +
-            filter.tags.join(', ');
+        filter_items.push(tag_desc);
     }
 
     // If there were any conditions to list, stich them in with an
     // intro.
-    if (filter_items !== '') {
-        html += 'You are subscribed to bugs that'+
-            '<ul class="bulleted">'+filter_items+'</ul>';
+    if (filter_items.length > 0) {
+        var ul = Y.Node.create('<ul class="bulleted"></ul>');
+        Y.each(filter_items, function (li) {ul.appendChild(li);});
+        description.appendChild(
+            Y.Node.create('<span>You are subscribed to bugs that</span>'));
+        description.appendChild(ul);
     }
 
     // Format event details.
+    var events; // When will email be sent?
     if (filter.bug_notification_level === 'Discussion') {
-        html += 'You will recieve an email when any change '+
+        events = 'You will recieve an email when any change '+
             'is made or a comment is added.';
     } else if (filter.bug_notification_level === 'Details') {
-        html += 'You will recieve an email when any changes '+
+        events = 'You will recieve an email when any changes '+
             'are made to the bug.  Bug comments will not be sent.';
     } else if (filter.bug_notification_level === 'Lifecycle') {
-        html += 'You will recieve an email when bugs are '+
+        events = 'You will recieve an email when bugs are '+
             'opened or closed.';
+    } else {
+        throw new Error('Unrecognized events.');
     }
-    return html;
+    description.appendChild(Y.Node.create(events));
+
+    return description;
 }
 
 /**

=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js	2011-03-30 15:23:42 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js	2011-04-01 16:08:35 +0000
@@ -147,11 +147,12 @@
         test_make_selector_controls: function() {
             // Verify the creation of select all/none controls.
             var selectors = module.make_selector_controls('sharona');
-            Assert.areEqual('sharona-select-all', selectors['all_name']);
-            Assert.areEqual('sharona-select-none', selectors['none_name']);
-            Assert.areEqual(
-                '<div id="sharona-selectors"',
-                selectors['html'].slice(0, 27));
+            Assert.areEqual(
+                'Select all', selectors.all_link.get('text'));
+            Assert.areEqual(
+                'Select none', selectors.none_link.get('text'));
+            Assert.areEqual(
+                'sharona-selectors', selectors.node.get('id'));
         }
     });
     suite.add(test_case);
@@ -419,8 +420,8 @@
             // accordion pane.
             module.setup(this.configuration);
             var checkboxes = Y.all('input[name="importances"]');
-            var select_all = Y.one('#importances-select-all');
-            var select_none = Y.one('#importances-select-none');
+            var select_all = Y.one('#importances-selectors > a.select-all');
+            var select_none = Y.one('#importances-selectors > a.select-none');
             Assert.isTrue(test_checked(checkboxes, true));
             // Simulate a click on the select_none control.
             Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click');
@@ -435,8 +436,8 @@
             // accordion pane.
             module.setup(this.configuration);
             var checkboxes = Y.all('input[name="statuses"]');
-            var select_all = Y.one('#statuses-select-all');
-            var select_none = Y.one('#statuses-select-none');
+            var select_all = Y.one('#statuses-selectors > a.select-all');
+            var select_none = Y.one('#statuses-selectors > a.select-none');
             Assert.isTrue(test_checked(checkboxes, true));
             // Simulate a click on the select_none control.
             Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click');

=== modified file 'lib/lp/registry/windmill/tests/test_plusnew_step2.py'
--- lib/lp/registry/windmill/tests/test_plusnew_step2.py	2011-02-10 04:00:00 +0000
+++ lib/lp/registry/windmill/tests/test_plusnew_step2.py	2011-04-01 16:08:35 +0000
@@ -37,7 +37,9 @@
 
         lpuser.SAMPLE_PERSON.ensure_login(self.client)
 
-        self.client.waits.forElement(id='field.displayname', timeout=u'20000')
+        self.client.open(url=u'%s/projects/+new'
+                        % RegistryWindmillLayer.base_url)
+        self.client.waits.forElement(id='field.displayname', timeout=u'200000')
         self.client.type(text=u'Badgers', id='field.displayname')
         self.client.type(text=u'badgers', id='field.name')
         self.client.type(text=u"There's the Badger", id='field.title')