Merge lp:~benji/launchpad/better-HTML-generation into lp:launchpad
- better-HTML-generation
- Merge into devel
Proposed by
Benji York
Status: | Merged |
---|---|
Approved by: | Benji York |
Approved revision: | no longer in the source branch. |
Merged at revision: | 12731 |
Proposed branch: | lp:~benji/launchpad/better-HTML-generation |
Merge into: | lp:launchpad |
Diff against target: |
930 lines (+311/-290) 2 files modified
lib/lp/registry/javascript/structural-subscription.js (+301/-281) lib/lp/registry/javascript/tests/test_structural_subscription.js (+10/-9) |
To merge this branch: | bzr merge lp:~benji/launchpad/better-HTML-generation |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email: mp+55960@code.launchpad.net |
Commit message
[r=bac][no-qa] Clean up HTML generation, primarily to avoid XSS.
Description of the change
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.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/registry/javascript/structural-subscription.js' |
2 | --- lib/lp/registry/javascript/structural-subscription.js 2011-03-30 15:23:42 +0000 |
3 | +++ lib/lp/registry/javascript/structural-subscription.js 2011-04-01 17:57:54 +0000 |
4 | @@ -11,9 +11,6 @@ |
5 | |
6 | var namespace = Y.namespace('lp.registry.structural_subscription'); |
7 | |
8 | -var INNER_HTML = 'innerHTML', |
9 | - VALUE = 'value'; |
10 | - |
11 | var FILTER_COMMENTS = 'filter-comments', |
12 | FILTER_WRAPPER = 'filter-wrapper', |
13 | ACCORDION_WRAPPER = 'accordion-wrapper', |
14 | @@ -21,8 +18,7 @@ |
15 | ADDED_OR_CHANGED = 'added-or-changed', |
16 | ADVANCED_FILTER = 'advanced-filter', |
17 | MATCH_ALL = 'match-all', |
18 | - MATCH_ANY = 'match-any', |
19 | - SS_COLLAPSIBLE = 'ss-collapsible' |
20 | + MATCH_ANY = 'match-any' |
21 | ; |
22 | |
23 | var add_subscription_overlay; |
24 | @@ -61,7 +57,7 @@ |
25 | function list_contains(list, target) { |
26 | // The list may be undefined in some cases. |
27 | return Y.Lang.isArray(list) && list.indexOf(target) !== -1; |
28 | -}; |
29 | +} |
30 | |
31 | // Expose to tests. |
32 | namespace._list_contains = list_contains; |
33 | @@ -209,8 +205,9 @@ |
34 | function save_subscription(form_data) { |
35 | var who; |
36 | var has_errors = check_for_errors_in_overlay(add_subscription_overlay); |
37 | - if (has_errors) |
38 | + if (has_errors) { |
39 | return false; |
40 | + } |
41 | if (form_data.recipient[0] === 'user') { |
42 | who = LP.links.me; |
43 | } else { |
44 | @@ -223,15 +220,18 @@ |
45 | |
46 | function check_for_errors_in_overlay(overlay) { |
47 | var has_errors = false; |
48 | - var errors = new Array(); |
49 | - for (var field in overlay.field_errors) { |
50 | - if (overlay.field_errors[field]) { |
51 | - has_errors = true; |
52 | - errors.push(field); |
53 | + var errors = []; |
54 | + var field; |
55 | + for (field in overlay.field_errors) { |
56 | + if (overlay.field_errors.hasOwnProperty(field)) { |
57 | + if (overlay.field_errors[field]) { |
58 | + has_errors = true; |
59 | + errors.push(field); |
60 | + } |
61 | } |
62 | - }; |
63 | + } |
64 | if (has_errors) { |
65 | - var error_text = errors.pop() |
66 | + var error_text = errors.pop(); |
67 | if (errors.length > 0) { |
68 | error_text = errors.join(', ') + ' and ' + error_text; |
69 | } |
70 | @@ -242,40 +242,38 @@ |
71 | } else { |
72 | return false; |
73 | } |
74 | -}; |
75 | +} |
76 | |
77 | /** |
78 | * Handle the activation of the edit subscription link. |
79 | */ |
80 | function edit_subscription_handler(context, form_data) { |
81 | var has_errors = check_for_errors_in_overlay(add_subscription_overlay); |
82 | - if (has_errors) |
83 | + var filter_id = '#filter-description-'+context.filter_id.toString(); |
84 | + if (has_errors) { |
85 | return false; |
86 | + } |
87 | var on = {success: function (new_data) { |
88 | var filter = new_data.getAttrs(); |
89 | - var description_node = Y.one( |
90 | - '#filter-description-'+context.filter_id.toString()); |
91 | - description_node.set( |
92 | - INNER_HTML, render_filter_description(filter)); |
93 | - var name_node = Y.one( |
94 | - '#filter-name-'+context.filter_id.toString()); |
95 | - name_node.set( |
96 | - INNER_HTML, render_filter_name(context.filter_info, filter)); |
97 | + var description_node = Y.one(filter_id); |
98 | + description_node |
99 | + .empty() |
100 | + .appendChild(create_filter_description(filter)); |
101 | + description_node.ancestor('.subscription-filter').one('.filter-name') |
102 | + .empty() |
103 | + .appendChild(render_filter_title(context.filter_info, filter)); |
104 | add_subscription_overlay.hide(); |
105 | }}; |
106 | patch_bug_filter(context.filter_info.filter, form_data, on); |
107 | } |
108 | |
109 | +/** |
110 | + * Initialize the overlay errors and set up field validators. |
111 | + */ |
112 | function setup_overlay_validators(overlay, overlay_id) { |
113 | - overlay.field_validators = { |
114 | - tags: get_error_for_tags_list |
115 | - }; |
116 | overlay.field_errors = {}; |
117 | - for (var field_name in overlay.field_validators) { |
118 | - add_input_validator(overlay, overlay_id, field_name, |
119 | - overlay.field_validators[field_name]); |
120 | - }; |
121 | -}; |
122 | + add_input_validator(overlay, overlay_id, 'tags', get_error_for_tags_list); |
123 | +} |
124 | |
125 | /** |
126 | * Populate the overlay element with the contents of the add/edit form. |
127 | @@ -295,8 +293,9 @@ |
128 | form_submit_callback: function(formdata) { |
129 | // Do not clean up if saving was not successful. |
130 | var save_succeeded = submit_callback(formdata); |
131 | - if (save_succeeded !== false) |
132 | + if (save_succeeded !== false) { |
133 | clean_up(); |
134 | + } |
135 | } |
136 | }); |
137 | add_subscription_overlay.render(content_box_id); |
138 | @@ -373,10 +372,14 @@ |
139 | * @param {String} name Name of the control. |
140 | */ |
141 | function make_cell(item, name) { |
142 | - return '<td style="padding-left:3px"><label><input type="checkbox" ' + |
143 | - 'name="' + name +'" ' + |
144 | - 'value="' + item + '" checked="checked">' + |
145 | - item + '</label><td>'; |
146 | + var cell = Y.Node.create('<td style="padding-left:3px"><td>'); |
147 | + cell.appendChild('<label></label>') |
148 | + .append(Y.Node.create('<input type="checkbox" checked></input>') |
149 | + .set('name', name) |
150 | + .set('value', item)) |
151 | + .append(Y.Node.create('<span></span>') |
152 | + .set('text', item)); |
153 | + return cell; |
154 | } |
155 | /** |
156 | * Make a table. |
157 | @@ -388,19 +391,15 @@ |
158 | * @param {Int} num_cols The number of columns for the table to use. |
159 | */ |
160 | function make_table(list, name, num_cols) { |
161 | - var html = '<table>'; |
162 | - var i; |
163 | + var table = Y.Node.create('<table></table>'); |
164 | + var i, row; |
165 | for (i=0; i<list.length; i++) { |
166 | if (i % num_cols === 0) { |
167 | - if (i !== 0) { |
168 | - html += '</tr>'; |
169 | - } |
170 | - html += '<tr>'; |
171 | + row = table.appendChild('<tr></tr>'); |
172 | } |
173 | - html += make_cell(list[i], name); |
174 | + row.appendChild(make_cell(list[i], name)); |
175 | } |
176 | - html += '</tr></table>'; |
177 | - return html; |
178 | + return table; |
179 | } |
180 | |
181 | /** |
182 | @@ -413,16 +412,18 @@ |
183 | * @return {Object} Hash with 'all_name', 'none_name', and 'html' keys. |
184 | */ |
185 | function make_selector_controls(parent) { |
186 | + var selectors_id = parent + '-selectors'; |
187 | var rv = {}; |
188 | - rv.all_name = parent + '-select-all'; |
189 | - rv.none_name = parent + '-select-none'; |
190 | - rv.html = '<div id="'+ parent + '-selectors" '+ |
191 | - 'style="margin-left: 10px;margin-bottom: 10px">' + |
192 | - ' <a href="#" id="' + rv.all_name + |
193 | - '">Select all</a> ' + |
194 | - ' <a href="#" id="' + rv.none_name + |
195 | - '">Select none</a>' + |
196 | - '</div>'; |
197 | + rv.all_link = Y.Node.create( |
198 | + '<a href="#" class="select-all">Select all</a>'); |
199 | + rv.none_link = Y.Node.create( |
200 | + '<a href="#" class="select-none">Select none</a>'); |
201 | + rv.node = Y.Node.create( |
202 | + '<div style="margin-left: 10px;margin-bottom: 10px"></div>') |
203 | + .set('id', selectors_id) |
204 | + .append(rv.all_link) |
205 | + .append(' ') |
206 | + .append(rv.none_link); |
207 | |
208 | return rv; |
209 | } |
210 | @@ -474,27 +475,23 @@ |
211 | id: "tags_ai", |
212 | contentHeight: {method: "auto"} |
213 | } ); |
214 | - |
215 | tags_container = tags_ai; |
216 | - |
217 | - tags_ai.set("bodyContent", |
218 | - '<div>\n' + |
219 | - '<div>\n' + |
220 | - ' <input type="radio" name="tag_match" value="' + |
221 | - MATCH_ALL + '" checked> Match all tags\n' + |
222 | - ' <input type="radio" name="tag_match" value="' + |
223 | - MATCH_ANY + '"> Match any tags\n' + |
224 | - '</div>\n' + |
225 | - '<div style="padding-bottom:10px;">\n' + |
226 | - ' <input type="text" name="tags" size="60"/>\n' + |
227 | + tags_ai.set("bodyContent", Y.Node.create('<div><div></div></div>') |
228 | + .append(Y.Node.create('<input type="radio" name="tag_match" checked>') |
229 | + .set('value', MATCH_ALL)) |
230 | + .append('Match all tags') |
231 | + .append(Y.Node.create('<input type="radio" name="tag_match" checked>') |
232 | + .set('value', MATCH_ANY)) |
233 | + .append('Match any tags') |
234 | + .append( |
235 | + '<div style="padding-bottom:10px;">' + |
236 | + ' <input type="text" name="tags" size="60"/>' + |
237 | ' <a target="help"'+ |
238 | ' href="/+help/structural-subscription-tags.html" ' + |
239 | ' class="sprite maybe"> '+ |
240 | '<span class="invisible-link">Structural subscription tags '+ |
241 | - ' help</span></a>\n ' + |
242 | - '</div>\n' + |
243 | - '</div>\n'); |
244 | - |
245 | + ' help</span></a> ' + |
246 | + '</div>')); |
247 | accordion.addItem(tags_ai); |
248 | |
249 | // Build importances pane. |
250 | @@ -507,20 +504,17 @@ |
251 | } ); |
252 | var importances = LP.cache.importances; |
253 | var selectors = make_selector_controls('importances'); |
254 | - var importances_html = '<div id="importances-wrapper">' + |
255 | - selectors.html + |
256 | - make_table(importances, 'importances', 4) + |
257 | - '</div>'; |
258 | - importances_ai.set("bodyContent", importances_html); |
259 | + importances_ai.set("bodyContent", |
260 | + Y.Node.create('<div id="importances-wrapper"></div>') |
261 | + .append(selectors.node) |
262 | + .append(make_table(importances, 'importances', 4))); |
263 | accordion.addItem(importances_ai); |
264 | // Wire up the 'all' and 'none' selectors. |
265 | - var all_link = content_node.one('#' + selectors.all_name); |
266 | - var none_link = Y.one('#' + selectors.none_name); |
267 | var node = content_node.one('#importances-wrapper'); |
268 | - var select_all_handler = make_select_handler(node, importances, true); |
269 | - var select_none_handler = make_select_handler(node, importances, false); |
270 | - all_link.on('click', select_all_handler); |
271 | - none_link.on('click', select_none_handler); |
272 | + selectors.all_link.on('click', |
273 | + make_select_handler(node, importances, true)); |
274 | + selectors.none_link.on('click', |
275 | + make_select_handler(node, importances, false)); |
276 | |
277 | // Build statuses pane. |
278 | statuses_ai = new Y.AccordionItem( { |
279 | @@ -532,18 +526,17 @@ |
280 | } ); |
281 | var statuses = LP.cache.statuses; |
282 | selectors = make_selector_controls('statuses'); |
283 | - var status_html = '<div id="statuses-wrapper">' + |
284 | - selectors.html + make_table(statuses, 'statuses', 3)+ |
285 | - '</div>'; |
286 | - statuses_ai.set("bodyContent", status_html); |
287 | + statuses_ai.set("bodyContent", |
288 | + Y.Node.create('<div id="statuses-wrapper"></div>') |
289 | + .append(selectors.node) |
290 | + .append(make_table(statuses, 'statuses', 3))); |
291 | accordion.addItem(statuses_ai); |
292 | - all_link = content_node.one('#' + selectors.all_name); |
293 | - none_link = Y.one('#' + selectors.none_name); |
294 | + // Wire up the 'all' and 'none' selectors. |
295 | node = content_node.one('#statuses-wrapper'); |
296 | - select_all_handler = make_select_handler(node, statuses, true); |
297 | - select_none_handler = make_select_handler(node, statuses, false); |
298 | - all_link.on('click', select_all_handler); |
299 | - none_link.on('click', select_none_handler); |
300 | + selectors.all_link.on('click', |
301 | + make_select_handler(node, statuses, true)); |
302 | + selectors.none_link.on('click', |
303 | + make_select_handler(node, statuses, false)); |
304 | |
305 | return accordion; |
306 | } |
307 | @@ -624,105 +617,125 @@ |
308 | } |
309 | |
310 | /** |
311 | + * Add a recipient picker to the overlay. |
312 | + */ |
313 | +function add_recipient_picker(content_box, hide) { |
314 | + var no_recipient_picker = Y.Node.create( |
315 | + '<input type="hidden" name="recipient" value="user">' + |
316 | + '<span>Yourself</span>'); |
317 | + var recipient_picker = Y.Node.create( |
318 | + '<label><input type="radio" name="recipient" value="user" checked>' + |
319 | + ' Yourself</label><br>' + |
320 | + '<label><input type="radio" name="recipient" value="team">' + |
321 | + ' One of the teams you administer</label><br>' + |
322 | + '<dl style="margin-left:25px;">' + |
323 | + ' <dt></dt>' + |
324 | + ' <dd>' + |
325 | + ' <select name="team" id="structural-subscription-teams">' + |
326 | + ' </select>' + |
327 | + ' </dd>' + |
328 | + '</dl>'); |
329 | + var teams = LP.cache.administratedTeams; |
330 | + var node = content_box.one('#bug-mail-recipient'); |
331 | + node.empty(); |
332 | + // Populate the team drop down from LP.cache data, if appropriate. |
333 | + if (!hide && teams.length > 0) { |
334 | + var select = recipient_picker.one('#structural-subscription-teams'); |
335 | + var i; |
336 | + for (i=0; i<teams.length; i++) { |
337 | + select.append(Y.Node.create('<option></option>') |
338 | + .set('text', teams[i].title) |
339 | + .set('value', teams[i].link)); |
340 | + } |
341 | + select.on( |
342 | + 'focus', |
343 | + function () { |
344 | + Y.one('input[value="team"][name="recipient"]').set( |
345 | + 'checked', true); |
346 | + } |
347 | + ); |
348 | + node.append(recipient_picker); |
349 | + } else { |
350 | + node.append(no_recipient_picker); |
351 | + } |
352 | +} |
353 | + |
354 | +/** |
355 | * Construct the overlay and populate it with the add/edit form. |
356 | */ |
357 | function setup_overlay(content_box_id, hide_recipient_picker) { |
358 | var content_node = Y.one(content_box_id); |
359 | - var container = Y.Node.create('<div id="overlay-container"></div>'); |
360 | - var accordion_overlay_id = 'accordion-overlay'; |
361 | - var teams = LP.cache.administratedTeams; |
362 | - var no_recipient_picker = |
363 | - ' <input type="hidden" name="recipient" value="user">\n' + |
364 | - ' <span>Yourself</span>\n', |
365 | - recipient_picker = |
366 | - ' <input type="radio" name="recipient" value="user"\n'+ |
367 | - ' id="structural-subscription-recipient-user" checked>\n'+ |
368 | - ' <label for="structural-subscription-recipient-user">\n'+ |
369 | - ' Yourself</label><br>\n' + |
370 | - ' <input type="radio" name="recipient"\n'+ |
371 | - ' id="structural-subscription-recipient-team"\n'+ |
372 | - ' value="team">\n'+ |
373 | - ' <label for="structural-subscription-recipient-team">One of\n'+ |
374 | - ' the teams you administer</label><br>\n' + |
375 | - ' <dl style="margin-left:25px;">\n' + |
376 | - ' <dt></dt>\n' + |
377 | - ' <dd>\n' + |
378 | - ' <select name="team" id="structural-subscription-teams">\n'+ |
379 | - ' </select>\n' + |
380 | - ' </dd>\n' + |
381 | - ' </dl>\n', |
382 | - control_code = |
383 | - '<dl>\n' + |
384 | - ' <dt>Bug mail recipient</dt>\n' + |
385 | - ' <dd>\n' + |
386 | - ((!hide_recipient_picker && teams.length > 0) ? |
387 | - recipient_picker : no_recipient_picker) + |
388 | - ' </dd>\n' + |
389 | - ' <dt>Subscription name</dt>\n' + |
390 | - ' <dd>\n' + |
391 | - ' <input type="text" name="name">\n' + |
392 | - ' <a target="help" class="sprite maybe"\n' + |
393 | - ' href="/+help/structural-subscription-name.html"> \n' + |
394 | - ' <span class="invisible-link">Structural subscription\n'+ |
395 | - ' description help</span></a>\n ' + |
396 | - ' </dd>\n' + |
397 | - ' <dt>Receive mail for bugs affecting\n'+ |
398 | - ' <span id="structural-subscription-context-title">\n'+ |
399 | - ' '+LP.cache.context.title+'</span> that</dt>\n' + |
400 | - ' <dd>\n' + |
401 | - ' <div id="events">\n' + |
402 | - ' <input type="radio" name="events"\n' + |
403 | - ' value="' + ADDED_OR_CLOSED + '"\n'+ |
404 | - ' id="' + ADDED_OR_CLOSED + '" checked>\n'+ |
405 | - ' <label for="'+ADDED_OR_CLOSED+'">are added or '+ |
406 | - ' closed</label>\n'+ |
407 | - ' <br>\n' + |
408 | - ' <input type="radio" name="events"\n'+ |
409 | - ' value="' + ADDED_OR_CHANGED + '"\n' + |
410 | - ' id="' + ADDED_OR_CHANGED + '">\n'+ |
411 | - ' <label for="'+ADDED_OR_CHANGED+'">are added or changed in\n'+ |
412 | - ' any way\n'+ |
413 | - ' <em id="'+ADDED_OR_CHANGED+'-more">(more options...)</em>\n'+ |
414 | - ' </label>\n' + |
415 | - ' </div>\n' + |
416 | - ' <div id="' + FILTER_WRAPPER + '" class="ss-collapsible">\n' + |
417 | - ' <dl style="margin-left:25px;">\n' + |
418 | - ' <dt></dt>\n' + |
419 | - ' <dd>\n' + |
420 | - ' <input type="checkbox" name="filters"\n' + |
421 | - ' value="' + FILTER_COMMENTS + '"\n'+ |
422 | - ' id="'+FILTER_COMMENTS+'">\n' + |
423 | - ' <label for="'+FILTER_COMMENTS+'">Don\'t send mail about\n'+ |
424 | - ' comments</label><br>\n' + |
425 | - ' <input type="checkbox" name="filters"\n' + |
426 | - ' value="' + ADVANCED_FILTER + '"\n' + |
427 | - ' id="' + ADVANCED_FILTER + '">\n' + |
428 | - ' <label for="'+ADVANCED_FILTER+'">Bugs must match this\n'+ |
429 | - ' filter <em id="'+ADVANCED_FILTER+'-more">(...)</em>\n'+ |
430 | - ' </label><br>\n' + |
431 | - ' <div id="' + ACCORDION_WRAPPER + '" \n' + |
432 | - ' class="' + SS_COLLAPSIBLE + '">\n' + |
433 | - ' <dl>\n' + |
434 | - ' <dt></dt>\n' + |
435 | - ' <dd style="margin-left:25px;">\n' + |
436 | - ' <div id="' + accordion_overlay_id + '"\n' + |
437 | - ' style="position:relative; '+ |
438 | - 'overflow:hidden;"></div>\n' + |
439 | - ' </dd>\n' + |
440 | - ' </dl>\n' + |
441 | - ' </div> \n' + |
442 | - ' </dd>\n' + |
443 | - ' </dl>\n' + |
444 | - ' </div> \n' + |
445 | - ' </dd>\n' + |
446 | - ' <dt></dt>\n' + |
447 | - '</dl>'; |
448 | - |
449 | - content_node.appendChild(container); |
450 | - container.appendChild(Y.Node.create(control_code)); |
451 | - |
452 | - var accordion = create_accordion( |
453 | - '#' + accordion_overlay_id, content_node); |
454 | + var container = Y.Node.create( |
455 | + '<div id="overlay-container"><dl>' + |
456 | + ' <dt>Bug mail recipient</dt>' + |
457 | + ' <dd id="bug-mail-recipient">' + |
458 | + ' </dd>' + |
459 | + ' <dt>Subscription name</dt>' + |
460 | + ' <dd>' + |
461 | + ' <input type="text" name="name">' + |
462 | + ' <a target="help" class="sprite maybe"' + |
463 | + ' href="/+help/structural-subscription-name.html"> ' + |
464 | + ' <span class="invisible-link">Structural subscription' + |
465 | + ' description help</span></a> ' + |
466 | + ' </dd>' + |
467 | + ' <dt>Receive mail for bugs affecting' + |
468 | + ' <span id="structural-subscription-context-title"></span> that</dt>' + |
469 | + ' <dd>' + |
470 | + ' <div id="events">' + |
471 | + ' <input type="radio" name="events"' + |
472 | + ' value="added-or-closed"' + |
473 | + ' id="added-or-closed" checked>' + |
474 | + ' <label for="added-or-closed">are added or ' + |
475 | + ' closed</label>' + |
476 | + ' <br>' + |
477 | + ' <input type="radio" name="events"' + |
478 | + ' value="added-or-changed"' + |
479 | + ' id="added-or-changed">' + |
480 | + ' <label for="added-or-changed">are added or changed in' + |
481 | + ' any way' + |
482 | + ' <em id="added-or-changed-more">(more options...)</em>' + |
483 | + ' </label>' + |
484 | + ' </div>' + |
485 | + ' <div id="filter-wrapper" class="ss-collapsible">' + |
486 | + ' <dl style="margin-left:25px;">' + |
487 | + ' <dt></dt>' + |
488 | + ' <dd>' + |
489 | + ' <input type="checkbox" name="filters"' + |
490 | + ' value="filter-comments"' + |
491 | + ' id="filter-comments">' + |
492 | + ' <label for="filter-comments">Don\'t send mail about' + |
493 | + ' comments</label><br>' + |
494 | + ' <input type="checkbox" name="filters"' + |
495 | + ' value="advanced-filter"' + |
496 | + ' id="advanced-filter">' + |
497 | + ' <label for="advanced-filter">Bugs must match this' + |
498 | + ' filter <em id="advanced-filter-more">(...)</em>' + |
499 | + ' </label><br>' + |
500 | + ' <div id="accordion-wrapper" ' + |
501 | + ' class="ss-collapsible">' + |
502 | + ' <dl>' + |
503 | + ' <dt></dt>' + |
504 | + ' <dd style="margin-left:25px;">' + |
505 | + ' <div id="accordion-overlay"' + |
506 | + ' style="position:relative; overflow:hidden;"></div>' + |
507 | + ' </dd>' + |
508 | + ' </dl>' + |
509 | + ' </div> ' + |
510 | + ' </dd>' + |
511 | + ' </dl>' + |
512 | + ' </div> ' + |
513 | + ' </dd>' + |
514 | + ' <dt></dt>' + |
515 | + '</dl></div>'); |
516 | + |
517 | + // Assemble some nodes and set the title. |
518 | + content_node |
519 | + .appendChild(container) |
520 | + .one('#structural-subscription-context-title') |
521 | + .set('text', LP.cache.context.title); |
522 | + |
523 | + var accordion = create_accordion('#accordion-overlay', content_node); |
524 | + add_recipient_picker(container, hide_recipient_picker); |
525 | |
526 | // Set up click handlers for the events radio buttons. |
527 | var radio_group = Y.all('#events input'); |
528 | @@ -735,26 +748,6 @@ |
529 | advanced_filter.on( |
530 | 'change', |
531 | function() {handle_change(ADVANCED_FILTER, ACCORDION_WRAPPER);}); |
532 | - // Populate the team drop down from LP.cache data, if appropriate. |
533 | - if (!hide_recipient_picker && teams.length > 0) { |
534 | - var select = Y.one('#structural-subscription-teams'); |
535 | - var i; |
536 | - var team; |
537 | - for (i=0; i<teams.length; i++) { |
538 | - team = teams[i]; |
539 | - var option = Y.Node.create('<option></option>'); |
540 | - option.set(INNER_HTML, team.title); |
541 | - option.set(VALUE, team.link); |
542 | - select.appendChild(option); |
543 | - } |
544 | - select.on( |
545 | - 'focus', |
546 | - function () { |
547 | - Y.one('input[value="team"][name="recipient"]').set( |
548 | - 'checked', true); |
549 | - } |
550 | - ); |
551 | - } |
552 | return '#' + container._node.id; |
553 | } // setup_overlay |
554 | // Expose in the namespace for testing purposes. |
555 | @@ -807,7 +800,7 @@ |
556 | var overlay_id = setup_overlay(config.content_box, true); |
557 | var submit_button = Y.Node.create( |
558 | '<button type="submit" name="field.actions.create" ' + |
559 | - 'value="Save Changes" class="lazr-pos lazr-btn" '+ |
560 | + 'value="Save Changes" class="lazr-pos lazr-btn" ' + |
561 | '>OK</button>'); |
562 | // This is a bit of an odd approach, but it lets us retrofit code |
563 | // without a large refactoring. When edit_subscription_handler is |
564 | @@ -848,8 +841,9 @@ |
565 | overlay.field_errors[field_name] = true; |
566 | // Accordion sets fixed height for the accordion item, |
567 | // so we have to resize the tags container. |
568 | - if (field_name == 'tags') |
569 | + if (field_name === 'tags') { |
570 | tags_container.resize(); |
571 | + } |
572 | // Firefox prohibits focus from inside the 'focus lost' event |
573 | // handler (probably to stop loops), so we need to run |
574 | // it from a different context (which we do with setTimeout). |
575 | @@ -859,7 +853,7 @@ |
576 | overlay.field_errors[field_name] = false; |
577 | } |
578 | }); |
579 | -}; |
580 | +} |
581 | |
582 | function get_error_for_tags_list(value) { |
583 | // See database/schema/trusted.sql valid_name() function |
584 | @@ -874,7 +868,8 @@ |
585 | 'digits 0-9 and symbols "+", "-" or ".", and they ' + |
586 | 'must start with a lowercase letter or a digit.'); |
587 | } |
588 | -}; |
589 | +} |
590 | + |
591 | // Export for testing |
592 | namespace._get_error_for_tags_list = get_error_for_tags_list; |
593 | |
594 | @@ -956,12 +951,12 @@ |
595 | var i; |
596 | for (i=0; i<teams.length; i++) { |
597 | if (teams[i].link === filter_info.subscriber_link){ |
598 | - recipient_label.set(INNER_HTML, teams[i].title); |
599 | + recipient_label.set('text', teams[i].title); |
600 | break; |
601 | } |
602 | } |
603 | } else { |
604 | - recipient_label.set(INNER_HTML, 'Yourself'); |
605 | + recipient_label.set('text', 'Yourself'); |
606 | } |
607 | content_node.one('[name="name"]').set('value',filter.description); |
608 | if (is_lifecycle) { |
609 | @@ -1011,10 +1006,10 @@ |
610 | context.filter_info = filter_info; |
611 | context.filter_id = filter_id; |
612 | var title = subscription.target_title; |
613 | - Y.one('#structural-subscription-context-title').set( |
614 | - INNER_HTML, title); |
615 | - Y.one('#subscription-overlay-title').set( |
616 | - INNER_HTML, 'Edit subscription for '+title+' bugs'); |
617 | + Y.one('#structural-subscription-context-title') |
618 | + .set('text', title); |
619 | + Y.one('#subscription-overlay-title') |
620 | + .set('text', 'Edit subscription for '+title+' bugs'); |
621 | add_subscription_overlay.show(); |
622 | } |
623 | }; |
624 | @@ -1035,14 +1030,14 @@ |
625 | headers: {'X-HTTP-Method-Override': 'DELETE'}, |
626 | on: {success: function(transactionid, response, args){ |
627 | var subscriber = Y.one( |
628 | - '#subscription-'+subscriber_id.toString()), |
629 | - node = subscriber, |
630 | - filters = subscriber.all('.subscription-filter'); |
631 | + '#subscription-'+subscriber_id.toString()); |
632 | + var to_collapse = subscriber; |
633 | + var filters = subscriber.all('.subscription-filter'); |
634 | if (!filters.isEmpty()) { |
635 | - node = Y.one( |
636 | + to_collapse = Y.one( |
637 | '#subscription-filter-'+filter_id.toString()); |
638 | } |
639 | - collapse_node(node); |
640 | + collapse_node(to_collapse); |
641 | }, |
642 | failure: error_handler.getFailureHandler() |
643 | } |
644 | @@ -1066,11 +1061,12 @@ |
645 | var filter_info = sub.filters[j]; |
646 | if (!filter_info.subscriber_is_team || |
647 | filter_info.user_is_team_admin) { |
648 | - var edit_link = Y.one('#edit-'+filter_id.toString()); |
649 | + var node = Y.one('#subscription-filter-'+filter_id.toString()); |
650 | + var edit_link = node.one('a.edit-subscription'); |
651 | var edit_handler = make_edit_handler( |
652 | sub, filter_info, filter_id, config, context); |
653 | edit_link.on('click', edit_handler); |
654 | - var delete_link = Y.one('#unsubscribe-'+filter_id.toString()); |
655 | + var delete_link = node.one('a.delete-subscription'); |
656 | var delete_handler = make_delete_handler( |
657 | filter_info.filter, filter_id, i); |
658 | delete_link.on('click', delete_handler); |
659 | @@ -1085,22 +1081,27 @@ |
660 | */ |
661 | function fill_in_bug_subscriptions(config, context) { |
662 | validate_config(config); |
663 | + |
664 | var listing = Y.one('#subscription-listing'); |
665 | var subscription_info = LP.cache.subscription_info; |
666 | - var html = '<div class="yui-g"><div id="structural-subscriptions">'; |
667 | + var top_node = Y.Node.create( |
668 | + '<div class="yui-g"><div id="structural-subscriptions"></div></div>'); |
669 | var filter_id = 0; |
670 | var i; |
671 | var j; |
672 | for (i=0; i<subscription_info.length; i++) { |
673 | var sub = subscription_info[i]; |
674 | - html += |
675 | + var sub_node = top_node.appendChild(Y.Node.create( |
676 | '<div style="margin-top: 2em; padding: 0 1em 1em 1em; '+ |
677 | - ' border: 1px solid #ddd;"'+ |
678 | - ' id="subscription-'+i.toString()+'">'+ |
679 | - ' <span style="float: left; margin-top: -0.6em; padding: 0 1ex;'+ |
680 | - ' background-color: #fff;">Subscriptions to'+ |
681 | - ' <a href="'+sub.target_url+'">'+sub.target_title+'</a>'+ |
682 | - ' </span>'; |
683 | + ' border: 1px solid #ddd;"></div>') |
684 | + .set('id', 'subscription-'+i.toString())); |
685 | + sub_node.appendChild(Y.Node.create( |
686 | + ' <span style="float: left; margin-top: -0.6em; '+ |
687 | + ' padding: 0 1ex; background-color: #fff;"></a>')) |
688 | + .appendChild('<span>Subscriptions to </span>') |
689 | + .appendChild(Y.Node.create('<a></a>') |
690 | + .set('href', sub.target_url) |
691 | + .set('text', sub.target_title)); |
692 | |
693 | for (j=0; j<sub.filters.length; j++) { |
694 | var filter = sub.filters[j].filter; |
695 | @@ -1110,52 +1111,53 @@ |
696 | // and see the information you expect to see. |
697 | LP.cache['structural-subscription-filter-'+filter_id.toString()] = |
698 | filter; |
699 | - html += |
700 | + var filter_node = sub_node.appendChild(Y.Node.create( |
701 | '<div style="margin: 1em 0em 0em 1em"'+ |
702 | - ' id="subscription-filter-'+filter_id.toString()+'"'+ |
703 | - ' class="subscription-filter">'+ |
704 | - ' <div style="margin-top: 1em">'+ |
705 | - ' <strong id="filter-name-'+ |
706 | - filter_id.toString()+'">'+ |
707 | - render_filter_name(sub.filters[j], filter)+'</strong>'; |
708 | + ' class="subscription-filter"></div>') |
709 | + .set('id', 'subscription-filter-'+filter_id.toString())) |
710 | + .appendChild(Y.Node.create( |
711 | + '<div style="margin-top: 1em"></div>')); |
712 | + filter_node.appendChild(Y.Node.create( |
713 | + '<strong class="filter-name"></strong>')) |
714 | + .appendChild(render_filter_title(sub.filters[j], filter)); |
715 | + |
716 | if (!sub.filters[j].subscriber_is_team || |
717 | sub.filters[j].user_is_team_admin) { |
718 | // User can edit the subscription. |
719 | - html += |
720 | + filter_node.appendChild(Y.Node.create( |
721 | '<span style="float: right">'+ |
722 | - '<a href="#" class="sprite modify edit js-action"'+ |
723 | - ' id="edit-'+filter_id.toString()+'">'+ |
724 | + '<a href="#" class="sprite modify edit js-action '+ |
725 | + ' edit-subscription">'+ |
726 | ' Edit this subscription</a> or '+ |
727 | - '<a href="#" class="sprite modify remove js-action"'+ |
728 | - ' id="unsubscribe-'+filter_id.toString()+'">'+ |
729 | - ' Unsubscribe</a></span>'; |
730 | + '<a href="#" class="sprite modify remove js-action '+ |
731 | + ' delete-subscription">'+ |
732 | + ' Unsubscribe</a></span>')); |
733 | } else { |
734 | // User cannot edit the subscription, because this is a |
735 | // team and the user does not have admin privileges. |
736 | - html += |
737 | + filter_node.appendChild(Y.Node.create( |
738 | '<span style="float: right"><em>'+ |
739 | 'You do not have privileges to change this subscription'+ |
740 | - '</em></span>'; |
741 | + '</em></span>')); |
742 | } |
743 | - html += '</div>'; |
744 | - html += |
745 | - '<div style="padding-left: 1em"'+ |
746 | - ' id="filter-description-'+filter_id.toString()+'">'+ |
747 | - render_filter_description(filter)+'</div>'; |
748 | - |
749 | - html += '</div>'; |
750 | + |
751 | + filter_node.appendChild(Y.Node.create( |
752 | + '<div style="padding-left: 1em"></div>') |
753 | + .set('id', 'filter-description-'+filter_id.toString())) |
754 | + .appendChild(create_filter_description(filter)); |
755 | + |
756 | filter_id += 1; |
757 | } |
758 | |
759 | // We can remove this once we enforce at least one filter per |
760 | // subscription. |
761 | if (subscription_info[i].filters.length === 0) { |
762 | - html += '<strong>All messages</strong>'; |
763 | + sub_node.appendChild( |
764 | + '<div style="clear: both; padding: 1em 0 0 1em"></div>') |
765 | + .appendChild('<strong>All messages</strong>'); |
766 | } |
767 | - html += '</div>'; |
768 | } |
769 | - html += '</div></div>'; |
770 | - listing.appendChild(Y.Node.create(html)); |
771 | + listing.appendChild(top_node); |
772 | |
773 | wire_up_edit_links(config, context); |
774 | } |
775 | @@ -1163,7 +1165,8 @@ |
776 | /** |
777 | * Construct a one-line textual description of a filter's name. |
778 | */ |
779 | -function render_filter_name(filter_info, filter) { |
780 | +function render_filter_title(filter_info, filter) { |
781 | + var title = Y.Node.create('<span></span>'); |
782 | var description; |
783 | if (filter.description) { |
784 | description = '"'+filter.description+'"'; |
785 | @@ -1171,62 +1174,79 @@ |
786 | description = '(unnamed)'; |
787 | } |
788 | if (filter_info.subscriber_is_team) { |
789 | - return '<a href="'+filter_info.subscriber_url+'">'+ |
790 | - filter_info.subscriber_title+"</a> subscription: "+description; |
791 | + title.appendChild(Y.Node.create('<a></a>')) |
792 | + .set('href', filter_info.subscriber_url) |
793 | + .set('text', filter_info.subscriber_title); |
794 | + title.appendChild(Y.Node.create('<span></span>')) |
795 | + .set('text', ' subscription: '+description); |
796 | } else { |
797 | - return 'Your subscription: '+description; |
798 | + title.set('text', 'Your subscription: '+description); |
799 | } |
800 | + return title; |
801 | } |
802 | |
803 | /** |
804 | * Construct a textual description of all of filter's properties. |
805 | */ |
806 | -function render_filter_description(filter) { |
807 | - var html = ''; |
808 | - var filter_items = ''; |
809 | +function create_filter_description(filter) { |
810 | + var description = Y.Node.create('<div></div>'); |
811 | + |
812 | + var filter_items = []; |
813 | // Format status conditions. |
814 | if (filter.statuses.length !== 0) { |
815 | - filter_items += '<li> have status ' + |
816 | - filter.statuses.join(', '); |
817 | + filter_items.push(Y.Node.create('<li></li>') |
818 | + .set('text', 'have status '+filter.statuses.join(', '))); |
819 | } |
820 | |
821 | // Format importance conditions. |
822 | if (filter.importances.length !== 0) { |
823 | - filter_items += '<li> are of importance ' + |
824 | - filter.importances.join(', '); |
825 | + filter_items.push(Y.Node.create('<li></li>') |
826 | + .set('text', 'are of importance '+filter.importances.join(', '))); |
827 | } |
828 | |
829 | // Format tag conditions. |
830 | if (filter.tags.length !== 0) { |
831 | - filter_items += '<li> are tagged with '; |
832 | + var tag_desc = Y.Node.create('<li>are tagged with </li>') |
833 | + .append(Y.Node.create('<strong></strong>')) |
834 | + .append(Y.Node.create('<span> of these tags: </span>')) |
835 | + .append(Y.Node.create('<span></span>') |
836 | + .set('text', filter.tags.join(', '))); |
837 | + |
838 | if (filter.find_all_tags) { |
839 | - filter_items += '<strong>all</strong>'; |
840 | + tag_desc.one('strong').set('text', 'all'); |
841 | } else { |
842 | - filter_items += '<strong>any</strong>'; |
843 | + tag_desc.one('strong').set('text', 'any'); |
844 | } |
845 | - filter_items += ' of these tags: ' + |
846 | - filter.tags.join(', '); |
847 | + filter_items.push(tag_desc); |
848 | } |
849 | |
850 | // If there were any conditions to list, stich them in with an |
851 | // intro. |
852 | - if (filter_items !== '') { |
853 | - html += 'You are subscribed to bugs that'+ |
854 | - '<ul class="bulleted">'+filter_items+'</ul>'; |
855 | + if (filter_items.length > 0) { |
856 | + var ul = Y.Node.create('<ul class="bulleted"></ul>'); |
857 | + Y.each(filter_items, function (li) {ul.appendChild(li);}); |
858 | + description.appendChild( |
859 | + Y.Node.create('<span>You are subscribed to bugs that</span>')); |
860 | + description.appendChild(ul); |
861 | } |
862 | |
863 | // Format event details. |
864 | + var events; // When will email be sent? |
865 | if (filter.bug_notification_level === 'Discussion') { |
866 | - html += 'You will recieve an email when any change '+ |
867 | + events = 'You will recieve an email when any change '+ |
868 | 'is made or a comment is added.'; |
869 | } else if (filter.bug_notification_level === 'Details') { |
870 | - html += 'You will recieve an email when any changes '+ |
871 | + events = 'You will recieve an email when any changes '+ |
872 | 'are made to the bug. Bug comments will not be sent.'; |
873 | } else if (filter.bug_notification_level === 'Lifecycle') { |
874 | - html += 'You will recieve an email when bugs are '+ |
875 | + events = 'You will recieve an email when bugs are '+ |
876 | 'opened or closed.'; |
877 | + } else { |
878 | + throw new Error('Unrecognized events.'); |
879 | } |
880 | - return html; |
881 | + description.appendChild(Y.Node.create(events)); |
882 | + |
883 | + return description; |
884 | } |
885 | |
886 | /** |
887 | |
888 | === modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js' |
889 | --- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-03-30 15:23:42 +0000 |
890 | +++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-01 17:57:54 +0000 |
891 | @@ -147,11 +147,12 @@ |
892 | test_make_selector_controls: function() { |
893 | // Verify the creation of select all/none controls. |
894 | var selectors = module.make_selector_controls('sharona'); |
895 | - Assert.areEqual('sharona-select-all', selectors['all_name']); |
896 | - Assert.areEqual('sharona-select-none', selectors['none_name']); |
897 | - Assert.areEqual( |
898 | - '<div id="sharona-selectors"', |
899 | - selectors['html'].slice(0, 27)); |
900 | + Assert.areEqual( |
901 | + 'Select all', selectors.all_link.get('text')); |
902 | + Assert.areEqual( |
903 | + 'Select none', selectors.none_link.get('text')); |
904 | + Assert.areEqual( |
905 | + 'sharona-selectors', selectors.node.get('id')); |
906 | } |
907 | }); |
908 | suite.add(test_case); |
909 | @@ -419,8 +420,8 @@ |
910 | // accordion pane. |
911 | module.setup(this.configuration); |
912 | var checkboxes = Y.all('input[name="importances"]'); |
913 | - var select_all = Y.one('#importances-select-all'); |
914 | - var select_none = Y.one('#importances-select-none'); |
915 | + var select_all = Y.one('#importances-selectors > a.select-all'); |
916 | + var select_none = Y.one('#importances-selectors > a.select-none'); |
917 | Assert.isTrue(test_checked(checkboxes, true)); |
918 | // Simulate a click on the select_none control. |
919 | Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click'); |
920 | @@ -435,8 +436,8 @@ |
921 | // accordion pane. |
922 | module.setup(this.configuration); |
923 | var checkboxes = Y.all('input[name="statuses"]'); |
924 | - var select_all = Y.one('#statuses-select-all'); |
925 | - var select_none = Y.one('#statuses-select-none'); |
926 | + var select_all = Y.one('#statuses-selectors > a.select-all'); |
927 | + var select_none = Y.one('#statuses-selectors > a.select-none'); |
928 | Assert.isTrue(test_checked(checkboxes, true)); |
929 | // Simulate a click on the select_none control. |
930 | Y.Event.simulate(Y.Node.getDOMNode(select_none), 'click'); |
This is a vast improvement over our original way of creating the html content for the overlay. Thanks for cleaning it all up.
As we discussed on IRC I am a little concerned that there are still large sections of string concatenation to create the recipient picker and overlay container. You pointed out it is all constant string values so it shouldn't present a problem. In the future we can always break those sections down if it proves to be needed.