Merge lp:~wallyworld/launchpad/blueprint-subscriptions into lp:launchpad

Proposed by Ian Booth
Status: Work in progress
Proposed branch: lp:~wallyworld/launchpad/blueprint-subscriptions
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/refactor-bugs-subscriber-javascript
Diff against target: 1337 lines (+1257/-3)
8 files modified
lib/lp/blueprints/javascript/blueprint_index.js (+20/-0)
lib/lp/blueprints/javascript/blueprint_index_portlets.js (+853/-0)
lib/lp/blueprints/javascript/tests/test_subscription_portlet.html (+47/-0)
lib/lp/blueprints/javascript/tests/test_subscription_portlet.js (+285/-0)
lib/lp/blueprints/templates/specification-index.pt (+2/-2)
lib/lp/blueprints/templates/specification-portlet-subscribers.pt (+44/-1)
lib/lp/blueprints/templates/specification-subscriber-row.pt (+2/-0)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/blueprint-subscriptions
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+64010@code.launchpad.net

Description of the change

The is branch #4 in a series of changes to improve blueprint subscription functionality. It builds on the work done previously which:
- exposes blueprint subscribe/unsubscribe methods to the web service
- refactors the blueprint tales to allow snippets of subscription data to be accessible to the ajax client
- refactors some bugs javascript to allow sharing of code with this branch

This branch adds support for subscribing and unsubscribing from blueprints using ajax, including solving bug 50875 - "it is not possible to unsubscribe teams from blueprints."

== Implementation ==

The previous work means this branch can be entirely javascript. The implementation is based on the work done for the bugs subscriber portlet. However it is simpler since there's no direct vs indirect subscriptions. Plus there's significant changes compared to the bugs implementation. This branch delivers:
- ajax support for subscribing oneself or another person/team to the blueprint
- ajax support to unsubscribe a person/team/oneself via clicking the "Remove" icon next to the person in the subscribers list
- legacy support for editing a subscrription

This branch delivers usable functionality, but there's still work to do though as detailed below.

When subscribing to a blueprint, the subscription can be marked as "essential". This is supported in the following way:
- the initial ajax subscription will be non essential
- once subscribed, the subscription can be edited (via a link to a separate page) to turn the essential flag on/off as required.

The editing of the subscription is as per current behaviour.

The next branch will provide ajax form support for creating and maintaining subscriptions, removing the need for the old style html forms.

Because this branch was based on the work done for bug subscriptions, it has picked up the some of the same issues. The method for determining whether a user can unsubscribe another user/team is virtually unchanged from the bugs implementation. This code is buggy and it is possible in some limited circumstances a user will incorrectly be told they cannot unsubscribe someone when they should be able to. This occurs when the number of users in a team is greater than 500 and the current user is not in the first batch. This issue is currently being fixed for the bugs case. I will port across the fix once done.

Because of the above bug, and the fact that another branch or two or three will be required to finish the work, I've put this new ajax functionality behind a feature flag - disclosure.enhanced_blueprint_subscriptions.enabled

== Screenshot ==

http://people.canonical.com/~ianb/blueprint-subscription-portal.png

The screenshot shows how after subscribing oneself using the ajax link, a blue Edit Subscription link is provided to allow the subscription to be edited. It also shows the Remove icon for the subscribed user.

== Tests ==

The current tests for editing subscriptions via html forms were run. Additional javascript tests were written (test_subscription_portlet.js) to test aspects of the ajax behaviour:
- setting up the portlet infarstructure
- subscribing the current user
- unsubscribing the current user
- editing a subscription

It is not feasible to easily write javascript tests for the 'subscribe someone else' functionality. This uses a picker and the necessary internals to stub out the io calls are not exposed. The equivalent bugs tests are done in Windmill. Given the javascript and other aspects will be changing in the next branch, I propose delaying the completion of full test coverage until subsequent branches are delivered.

== Lint ==

jslint: No problem found in '/home/ian/projects/lp-branches/devel-sandbox/lib/lp/blueprints/javascript/blueprint_index.js'.
jslint: No problem found in '/home/ian/projects/lp-branches/devel-sandbox/lib/lp/blueprints/javascript/blueprint_index_portlets.js'.
jslint: No problem found in '/home/ian/projects/lp-branches/devel-sandbox/lib/lp/blueprints/javascript/tests/test_subscription_portlet.js'.

To post a comment you must log in.

Unmerged revisions

13183. By Ian Booth

Merge from trunk

13182. By Ian Booth

Merged refactor-bugs-subscriber-javascript into blueprint-subscriptions.

13181. By Ian Booth

Merged refactor-bugs-subscriber-javascript into blueprint-subscriptions.

13180. By Ian Booth

Merged refactor-bugs-subscriber-javascript into blueprint-subscriptions.

13179. By Ian Booth

Add feature flag and updates tests

13178. By Ian Booth

Add feature flag and updates tests

13177. By Ian Booth

Fix some bugs and add tests

13176. By Ian Booth

Merged refactor-bugs-subscriber-javascript into blueprint-subscriptions.

13175. By Ian Booth

Add some javascript tests

13174. By Ian Booth

Merge from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'lib/lp/blueprints/javascript'
=== added file 'lib/lp/blueprints/javascript/blueprint_index.js'
--- lib/lp/blueprints/javascript/blueprint_index.js 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/blueprint_index.js 2011-06-16 02:58:38 +0000
@@ -0,0 +1,20 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Form overlay widgets and subscriber handling for blueprint pages.
5 *
6 * @module blueprints
7 * @submodule blueprint_index
8 */
9
10YUI.add('lp.blueprints.blueprint_index', function(Y) {
11
12var namespace = Y.namespace('lp.blueprints.blueprint_index');
13
14namespace.setup_blueprint_index = function() {
15 // Register the YUI event handlers to respond to events generated when
16 // loading the subscription portlet.
17 Y.lp.blueprints.blueprint_index.portlets.setup_portlet_handlers();
18};
19
20}, "0.1", {"requires": ["base", "lp.blueprints.blueprint_index.portlets"]});
021
=== added file 'lib/lp/blueprints/javascript/blueprint_index_portlets.js'
--- lib/lp/blueprints/javascript/blueprint_index_portlets.js 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/blueprint_index_portlets.js 2011-06-16 02:58:38 +0000
@@ -0,0 +1,853 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Form overlay widgets and subscriber handling for blueprint pages.
5 *
6 * @module blueprints
7 * @submodule blueprint_index.portlets
8 */
9
10YUI.add('lp.blueprints.blueprint_index.portlets', function(Y) {
11
12var namespace = Y.namespace('lp.blueprints.blueprint_index.portlets');
13
14// The IO module to use.
15var YIO = Y;
16
17// The launchpad js client used.
18var lp_client;
19
20// The launchpad client entry for the current blueprint.
21var lp_blueprint_entry;
22
23// The blueprint itself, taken from cache.
24var blueprint_repr;
25
26var subscription_labels = Y.lp.app.subscriber.subscription_labels;
27
28// The set of subscriber CSS IDs as a JSON struct.
29var subscriber_ids;
30
31// We need to reset the onclick handler for the subscribe link until the
32// edit subscription overlays are done. So we need to keep a reference to the
33// current value.
34var subscribe_link_handler = undefined;
35
36/*
37 * An object representing the blueprint subscribers portlet.
38 *
39 * Since the portlet loads via XHR and inline subscribing
40 * depends on that portlet being loaded, setup a custom
41 * event object, to provide a hook for initializing subscription
42 * link callbacks after custom events.
43 */
44var PortletTarget = function() {};
45Y.augment(PortletTarget, Y.Event.Target);
46namespace.portlet = new PortletTarget();
47
48/*
49 * Create the lp client and bug entry if we haven't done so already.
50 *
51 * @method setup_client_and_bug
52 */
53function setup_client_and_blueprint() {
54 lp_client = new Y.lp.client.Launchpad();
55
56 if (blueprint_repr === undefined) {
57 blueprint_repr = LP.cache.context;
58 lp_blueprint_entry = new Y.lp.client.Entry(
59 lp_client, blueprint_repr, blueprint_repr.self_link);
60 }
61}
62
63namespace.load_subscribers_portlet = function(
64 subscription_link, subscription_link_handler) {
65 if (Y.UA.ie) {
66 return null;
67 }
68
69 Y.one('#subscribers-portlet-spinner').setStyle('display', 'block');
70
71 function hide_spinner() {
72 Y.one('#subscribers-portlet-spinner').setStyle('display', 'none');
73 // Fire a custom event to notify that the initial click
74 // handler on subscription_link set above should be
75 // cleared.
76 if (namespace) {
77 namespace.portlet.fire(
78 'blueprints:portletloadfailed', subscription_link_handler);
79 }
80 }
81
82 function setup_portlet(transactionid, response, args) {
83 hide_spinner();
84 Y.one('#portlet-subscribers')
85 .appendChild(Y.Node.create(response.responseText));
86
87 // Fire a custom portlet loaded event to notify when
88 // it's safe to setup subscriber link callbacks.
89 namespace.portlet.fire('blueprints:portletloaded');
90 }
91
92 var config = {on: {success: setup_portlet,
93 failure: hide_spinner}};
94 var url = Y.one('#subscribers-content-link').getAttribute('href');
95 YIO.io(url, config);
96};
97
98
99namespace.setup_portlet_handlers = function() {
100 namespace.portlet.subscribe('blueprints:portletloaded', function() {
101 load_subscriber_ids();
102 });
103 /*
104 * If the subscribers portlet fails to load, clear any
105 * click handlers, so the normal subscribe page can be reached.
106 */
107 namespace.portlet.subscribe('blueprints:portletloadfailed', function(click_handler) {
108 click_handler.detach();
109 });
110 namespace.portlet.subscribe('blueprints:portletsubscriberidsloaded', function() {
111 var subscription = get_subscribe_self_subscription();
112 setup_subscribe_me_handler(subscription);
113 setup_subscribe_someone_else_handler(subscription);
114 setup_unsubscribe_icon_handlers();
115 });
116
117
118 /*
119 * Subscribing someone else requires loading a grayed out
120 * username into the DOM until the subscribe action completes.
121 * There are a couple XHR requests in check_can_be_unsubscribed
122 * before the subscribe work can be done, so fire a custom event
123 * blueprints:nameloaded and do the work here when the event fires.
124 */
125 namespace.portlet.subscribe('blueprints:nameloaded', function(subscription) {
126 var error_handler = new Y.lp.client.ErrorHandler();
127 error_handler.clearProgressUI = function() {
128 var temp_link = Y.one('#temp-username');
129 if (temp_link) {
130 var temp_parent = temp_link.get('parentNode');
131 temp_parent.removeChild(temp_link);
132 }
133 };
134 error_handler.showError = function(error_msg) {
135 Y.lp.app.errors.display_error(
136 Y.one('.menu-link-addsubscriber'), error_msg);
137 };
138
139 var config = {
140 on: {
141 success: function(result) {
142 subscription.set('web_link', result.get('self_link'));
143 var temp_link = Y.one('#temp-username');
144 var temp_spinner = Y.one('#temp-name-spinner');
145 temp_link.removeChild(temp_spinner);
146 var person = subscription.get('person');
147 add_user_name_link(subscription);
148 Y.on('contentready', function() {
149 var temp_parent = temp_link.get('parentNode');
150 temp_parent.removeChild(temp_link);
151 }, '.' + person.get('css_name'));
152 },
153 failure: error_handler.getFailureHandler()
154 },
155 parameters: {
156 person: Y.lp.client.get_absolute_uri(
157 subscription.get('person').get('escaped_uri')),
158 suppress_notify: false
159 }
160 };
161 lp_client.named_post(blueprint_repr.self_link, 'subscribe', config);
162 });
163};
164
165function load_subscriber_ids() {
166 function on_success(transactionid, response, args) {
167 subscriber_ids = Y.JSON.parse(response.responseText);
168
169 // Fire a custom event to trigger the setting-up of the
170 // subscription handlers.
171 namespace.portlet.fire('blueprints:portletsubscriberidsloaded');
172 }
173
174 var config = {on: {success: on_success}};
175 var url = Y.one(
176 '#subscribers-ids-link').getAttribute('href');
177 YIO.io(url, config);
178}
179
180/*
181 * Set click handlers for unsubscribe remove icons.
182 *
183 * @method setup_unsubscribe_icon_handlers
184 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
185 */
186function setup_unsubscribe_icon_handlers() {
187 var subscription = new Y.lp.app.subscriber.Subscription({
188 link: Y.one('.menu-link-subscription'),
189 spinner: Y.one('#sub-unsub-spinner'),
190 subscriber: new Y.lp.app.subscriber.Subscriber({
191 uri: LP.links.me,
192 subscriber_ids: subscriber_ids
193 })
194 });
195
196 Y.on('click', function(e) {
197 e.halt();
198 unsubscribe_user_via_icon(e.target, subscription);
199 }, '.unsub-icon');
200}
201
202/*
203 * Set up and return a Subscription object for the direct subscription
204 * link.
205 */
206function get_subscribe_self_subscription() {
207 setup_client_and_blueprint();
208 var subscription = new Y.lp.app.subscriber.Subscription({
209 link: Y.one('.menu-link-subscription'),
210 spinner: Y.one('#sub-unsub-spinner'),
211 subscriber: new Y.lp.app.subscriber.Subscriber({
212 uri: LP.links.me,
213 subscriber_ids: subscriber_ids
214 })
215 });
216
217 subscription.set('can_be_unsubscribed', true);
218 subscription.set('person', subscription.get('subscriber'));
219 subscription.set('is_team', false);
220 return subscription;
221}
222
223/*
224 * Initialize callbacks for subscribe/unsubscribe links.
225 *
226 * @method setup_subscription_link_handlers
227 */
228function setup_subscribe_me_handler(subscription) {
229 if (LP.links.me === undefined) {
230 return;
231 }
232
233 if (subscription.is_node()) {
234 var subscribe_node = subscription.get('link');
235 var parent = subscribe_node.get('parentNode');
236 var is_subscribed = parent.hasClass('subscribed-true');
237 if (!is_subscribed) {
238 // We need to ensure we don't attach more than one handler so
239 // detach any existing one.
240 if (subscribe_link_handler !== undefined ) {
241 subscribe_link_handler.detach();
242 }
243 subscribe_link_handler = subscribe_node.on('click', function(e) {
244 e.halt();
245 subscription.set('can_be_unsubscribed', true);
246 subscription.set('person', subscription.get('subscriber'));
247 subscription.set('is_team', false);
248 var parent = e.target.get('parentNode');
249 if (parent.hasClass('subscribed-false')) {
250 subscribe_current_user(subscription);
251 }
252 else {
253 unsubscribe_current_user(subscription);
254 }
255 });
256 subscribe_node.addClass('js-action');
257 }
258 }
259}
260
261/*
262 * Add the user name to the subscriber's list.
263 *
264 * @method add_user_name_link
265 */
266function add_user_name_link(subscription) {
267 // Be paranoid about display_name, since timeouts or other errors
268 // could mean display_name wasn't set on initialization.
269 subscription.get('person').set_display_name(function () {
270 _add_user_name_link(subscription);
271 });
272}
273
274function _add_user_name_link(subscription) {
275 var person = subscription.get('person');
276 var display_user_function = function(link_node) {
277 var subscribers = Y.one('#subscribers-links');
278 if (subscription.is_current_user_subscribing()) {
279 // If this is the current user, then top post the name.
280 subscribers.insertBefore(
281 link_node, subscribers.get('firstChild'));
282 } else {
283 var next = get_next_subscriber_node(subscription);
284 if (next) {
285 subscribers.insertBefore(link_node, next);
286 } else {
287 // Handle the case of no subscribers.
288 var none_subscribers = Y.one('#none-subscribers');
289 if (none_subscribers) {
290 var none_parent = none_subscribers.get('parentNode');
291 none_parent.removeChild(none_subscribers);
292 }
293 subscribers.appendChild(link_node);
294 }
295 }
296
297 // Set the click handler if adding a remove icon.
298 if (subscription.can_be_unsubscribed_by_user()) {
299 var remove_icon =
300 Y.one('#unsubscribe-icon-' + person.get('css_name'));
301 remove_icon.on('click', function(e) {
302 e.halt();
303 unsubscribe_user_via_icon(e.target, subscription);
304 });
305 }
306 Y.lazr.anim.green_flash({node: link_node}).run();
307 };
308 build_user_link_html(subscription, display_user_function);
309}
310
311/*
312 * Unsubscribe a user from this blueprint when a remove icon is clicked.
313 *
314 * @method unsubscribe_user_via_icon
315 * @param icon {Node} The remove icon that was clicked.
316 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
317*/
318function unsubscribe_user_via_icon(icon, subscription) {
319 icon.set('src', '/@@/spinner');
320
321 var user_uri = get_user_uri_from_icon(icon);
322 var person = new Y.lp.app.subscriber.Subscriber({
323 uri: user_uri,
324 subscriber_ids: subscriber_ids
325 });
326 subscription.set('person', person);
327
328 var error_handler = new Y.lp.client.ErrorHandler();
329 error_handler.clearProgressUI = function () {
330 icon.set('src', '/@@/remove');
331 // Grab the icon again to reset to click handler.
332 var unsubscribe_icon = Y.one(
333 '#unsubscribe-icon-' + person.get('css_name'));
334 unsubscribe_icon.on('click', function(e) {
335 e.halt();
336 unsubscribe_user_via_icon(e.target, subscription);
337 });
338
339 };
340 error_handler.showError = function (error_msg) {
341 var flash_node = Y.one('.' + person.get('css_name'));
342 Y.lp.app.errors.display_error(flash_node, error_msg);
343
344 };
345
346 var subscription_link = subscription.get('link');
347 var config = {
348 on: {
349 success: function(id, response, args) {
350 Y.lp.app.subscribers_list.remove_user_link(person);
351 set_subscription_link_parent_class(subscription_link, false);
352 if (subscription.is_current_user_subscribing()) {
353 subscription.disable_spinner(
354 subscription_labels.SUBSCRIBE);
355 setup_subscribe_me_handler(subscription);
356 }
357 },
358
359 failure: error_handler.getFailureHandler()
360 }
361 };
362 if (!subscription.is_current_user_subscribing()) {
363 config.parameters = {
364 person: Y.lp.client.get_absolute_uri(user_uri)
365 };
366 }
367 lp_client.named_post(blueprint_repr.self_link, 'unsubscribe', config);
368}
369
370/*
371 * Subscribe the current user via the LP API.
372 *
373 * @method subscribe_current_user
374 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
375 */
376function subscribe_current_user(subscription) {
377 subscription.enable_spinner('Subscribing...');
378 var subscription_link = subscription.get('link');
379 var subscriber = subscription.get('subscriber');
380
381 var error_handler = new Y.lp.client.ErrorHandler();
382 error_handler.clearProgressUI = function () {
383 subscription.disable_spinner();
384 };
385 error_handler.showError = function (error_msg) {
386 Y.lp.app.errors.display_error(subscription_link, error_msg);
387 };
388
389 var config = {
390 on: {
391 success: function(result) {
392 subscription.set('web_link', result.get('self_link'));
393 if (subscribe_link_handler !== undefined ) {
394 subscribe_link_handler.detach();
395 }
396 subscription.disable_spinner(
397 subscription_labels.EDIT);
398 var subscribe_node = subscription.get('link');
399 subscribe_node.removeClass('remove')
400 .removeClass('js-action')
401 .addClass('edit');
402
403 set_subscription_link_parent_class(subscription_link, true);
404
405 // Handle the case where the subscriber's list displays
406 // "No subscribers."
407 var empty_subscribers = Y.one("#none-subscribers");
408 if (empty_subscribers) {
409 var parent = empty_subscribers.get('parentNode');
410 parent.removeChild(empty_subscribers);
411 }
412
413 add_user_name_link(subscription);
414 // Only when the link is added to the page, indicate success.
415 Y.on('contentready', function() {
416 var flash_node = Y.one('.' + subscriber.get('css_name'));
417 var anim = Y.lazr.anim.green_flash({ node: flash_node });
418 anim.run();
419 }, '.' + subscriber.get('css_name'));
420 },
421
422 failure: error_handler.getFailureHandler()
423 },
424
425 parameters: {
426 person: Y.lp.client.get_absolute_uri(
427 subscriber.get('escaped_uri'))
428 }
429 };
430 lp_client.named_post(blueprint_repr.self_link, 'subscribe', config);
431}
432
433/*
434 * Unsubscribe the current user via the LP API.
435 *
436 * @method unsubscribe_current_user
437 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
438 */
439function unsubscribe_current_user(subscription) {
440 subscription.enable_spinner('Unsubscribing...');
441 var subscription_link = subscription.get('link');
442 var subscriber = subscription.get('subscriber');
443
444 var error_handler = new Y.lp.client.ErrorHandler();
445 error_handler.clearProgressUI = function () {
446 subscription.disable_spinner();
447 };
448 error_handler.showError = function (error_msg) {
449 Y.lp.app.errors.display_error(subscription_link, error_msg);
450 };
451
452 var subscriber_link = Y.lp.client.get_absolute_uri(
453 subscriber.get('escaped_uri'));
454 var config = {
455 on: {
456 success: function(client) {
457 subscription.disable_spinner(
458 subscription_labels.SUBSCRIBE);
459 set_subscription_link_parent_class(
460 subscription_link, false);
461 Y.lp.app.subscribers_list.remove_user_link(subscriber);
462 },
463
464 failure: error_handler.getFailureHandler()
465 },
466
467 parameters: { person: subscriber_link }
468 };
469
470 lp_client.named_post(blueprint_repr.self_link, 'unsubscribe', config);
471}
472
473/*
474 * Initialize click handler for the subscribe someone else link.
475 *
476 * @method setup_subscribe_someone_else_handler
477 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
478 */
479function setup_subscribe_someone_else_handler(subscription) {
480 if (LP.links.me === undefined) {
481 return;
482 }
483 var config = {
484 header: 'Subscribe someone else',
485 step_title: 'Search',
486 picker_activator: '.menu-link-addsubscriber'
487 };
488
489 config.save = function(result) {
490 subscribe_someone_else(result, subscription);
491 };
492 var picker = Y.lp.app.picker.create('ValidPersonOrTeam', config);
493}
494
495/*
496 * Build the HTML for a user link for the subscribers list.
497 *
498 * @method build_user_link_html
499 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
500 * @param display_user_function {Object} A function to display the user node.
501 */
502function build_user_link_html(subscription, display_user_function) {
503 var error_handler = new Y.lp.client.ErrorHandler();
504 error_handler.showError = function (error_msg) {
505 var subscription_link = subscription.get('link');
506 Y.lp.app.errors.display_error(subscription_link, error_msg);
507
508 };
509
510 var config = {
511 on: {
512 success: function(id, response) {
513 var html = Y.Node.create(response.responseText);
514
515 // Override the node ids so that the remove icon is wired up
516 // correctly.
517 var person = subscription.get('person');
518 var css_name = person.get('css_name');
519 html.set('id', 'subscription-' + css_name);
520 var icon_link = html.one('a:nth-child(3)');
521 icon_link.set('id', 'unsubscribe-' + css_name);
522 icon_link.one('img')
523 .set('id', 'unsubscribe-icon-' + css_name);
524 display_user_function(html);
525 },
526
527 failure: error_handler.getFailureHandler()
528 }
529 };
530 // We need to retrieve the html to display from the server since we need
531 // to use specification subscription attributes not available on the
532 // client.
533 var url_base = subscription.get('web_link').replace('/api/devel', '');
534 var url = url_base + '/+blueprint-subscriber-row';
535 YIO.io(url, config);
536}
537
538/*
539 * Returns the next node in alphabetical order after the subscriber
540 * node now being added. No node is returned to append to end of list.
541 *
542 * The name can appear in one of two different lists. 1) The list of
543 * subscribers that can be unsubscribed by the current user, and
544 * 2) the list of subscribers that cannot be unsubscribed.
545 *
546 * @method get_next_subscriber_node
547 * @param subscription_link {Node} The sub/unsub link.
548 * @return {Node} The node appearing next in the subscriber list or
549 * undefined if no node is next.
550 */
551function get_next_subscriber_node(subscription) {
552 var full_name = subscription.get('person').get('full_display_name');
553 var can_be_unsubscribed = subscription.can_be_unsubscribed_by_user();
554 var nodes_by_name = {};
555 var unsubscribables = [];
556 var not_unsubscribables = [];
557
558 // Use the list of subscribers pulled from the DOM to have sortable
559 // lists of unsubscribable vs. not unsubscribable person links.
560 var all_subscribers = Y.all('#subscribers-links div');
561 if (all_subscribers.size() > 0) {
562 all_subscribers.each(function(sub_link) {
563 if (sub_link.getAttribute('id') !== 'temp-username') {
564 // User's displayname is found via the link's "name"
565 // attribute.
566 var sub_link_name = sub_link.one('a').getAttribute('name');
567 nodes_by_name[sub_link_name] = sub_link;
568 if (sub_link.one('img.unsub-icon')) {
569 unsubscribables.push(sub_link_name);
570 } else {
571 not_unsubscribables.push(sub_link_name);
572 }
573 }
574 });
575
576 // Add the current subscription.
577 if (can_be_unsubscribed) {
578 unsubscribables.push(full_name);
579 } else {
580 not_unsubscribables.push(full_name);
581 }
582 unsubscribables.sort();
583 not_unsubscribables.sort();
584 } else {
585 // If there is no all_subscribers, then we're dealing with
586 // the printed None, so return.
587 return undefined;
588 }
589
590 var i;
591 if ((!unsubscribables && !not_unsubscribables) ||
592 // If A) neither list exists, B) the user belongs in the second
593 // list but the second list doesn't exist, or C) user belongs in the
594 // first list and the second doesn't exist, return no node to append.
595 (!can_be_unsubscribed && !not_unsubscribables) ||
596 (can_be_unsubscribed && unsubscribables && !not_unsubscribables)) {
597 return undefined;
598 } else if (
599 // If the user belongs in the first list, and the first list
600 // doesn't exist, but the second one does, return the first node
601 // in the second list.
602 can_be_unsubscribed && !unsubscribables && not_unsubscribables) {
603 return nodes_by_name[not_unsubscribables[0]];
604 } else if (can_be_unsubscribed) {
605 // If the user belongs in the first list, loop the list for position.
606 for (i=0; i<unsubscribables.length; i++) {
607 if (unsubscribables[i] === full_name) {
608 if (i+1 < unsubscribables.length) {
609 return nodes_by_name[unsubscribables[i+1]];
610 // If the current link should go at the end of the first
611 // list and we're at the end of that list, return the
612 // first node of the second list. Due to earlier checks
613 // we can be sure this list exists.
614 } else if (i+1 >= unsubscribables.length) {
615 return nodes_by_name[not_unsubscribables[0]];
616 }
617 }
618 }
619 } else if (!can_be_unsubscribed) {
620 // If user belongs in the second list, loop the list for position.
621 for (i=0; i<not_unsubscribables.length; i++) {
622 if (not_unsubscribables[i] === full_name) {
623 if (i+1 < not_unsubscribables.length) {
624 return nodes_by_name[not_unsubscribables[i+1]];
625 } else {
626 return undefined;
627 }
628 }
629 }
630 }
631}
632
633/*
634 * Traverse the DOM of a given remove icon to find
635 * the user's link. Returns a URI of the form "/~username".
636 *
637 * @method get_user_uri_from_icon
638 * @param icon {Node} The node representing a remove icon.
639 * @return user_uri {String} The user's uri, without the hostname.
640 */
641function get_user_uri_from_icon(icon) {
642 var parent_div = icon.get('parentNode').get('parentNode');
643 // This should be parent_div.firstChild, but because of #text
644 // and cross-browser issues, using the YUI query syntax is
645 // safer here.
646 var user_uri = parent_div.one('a:nth-child(2)').getAttribute('href');
647
648 // Strip the domain off. We just want a path.
649 var host_start = user_uri.indexOf('//');
650 if (host_start !== -1) {
651 var host_end = user_uri.indexOf('/', host_start+2);
652 return user_uri.substring(host_end, user_uri.length);
653 }
654
655 return user_uri;
656}
657
658/*
659 * Set the class on subscription link's parentNode.
660 *
661 * This is used to reset the class used by the
662 * click handler to know which link was clicked.
663 *
664 * @method set_subscription_link_parent_class
665 * @param subscription_link {Node} The sub/unsub link.
666 * @param subscribed {Boolean} The sub/unsub'ed flag for the class.
667 */
668function set_subscription_link_parent_class(user_link, subscribed) {
669 var parent = user_link.get('parentNode');
670 if (subscribed) {
671 parent.removeClass('subscribed-false');
672 parent.addClass('subscribed-true');
673 } else {
674 parent.removeClass('subscribed-true');
675 parent.addClass('subscribed-false');
676 }
677}
678
679/*
680 * Subscribe a person or team other than the current user.
681 * This is a callback for the subscribe someone else picker.
682 *
683 * @method subscribe_someone_else
684 * @result {Object} The object representing a person returned by the API.
685 */
686function subscribe_someone_else(result, subscription) {
687 var person = new Y.lp.app.subscriber.Subscriber({
688 uri: result.api_uri,
689 display_name: result.title,
690 subscriber_ids: subscriber_ids
691 });
692 subscription.set('person', person);
693
694 var error_handler = new Y.lp.client.ErrorHandler();
695 error_handler.showError = function(error_msg) {
696 Y.lp.app.errors.display_error(
697 Y.one('.menu-link-addsubscriber'), error_msg);
698 };
699
700 if (subscription.is_already_subscribed()) {
701 error_handler.showError(
702 subscription.get('person').get('full_display_name') +
703 ' has already been subscribed');
704 } else {
705 check_can_be_unsubscribed(subscription);
706 }
707}
708
709/*
710 * Check if the current user can unsubscribe the person
711 * being subscribed.
712 *
713 * This must be done in JavaScript, since the subscription
714 * hasn't completed yet, and so, can_be_unsubscribed_by_user
715 * cannot be used.
716 *
717 * @method check_can_be_unsubscribed
718 * @param subscription {Object} A Y.lp.app.subscriber.Subscription object.
719 */
720function check_can_be_unsubscribed(subscription) {
721 var error_handler = new Y.lp.client.ErrorHandler();
722 error_handler.showError = function (error_msg) {
723 Y.lp.app.errors.display_error(
724 Y.one('.menu-link-addsubscriber'), error_msg);
725 };
726
727 var config = {
728 on: {
729 success: function(result) {
730 var is_team = result.get('is_team');
731 subscription.set('is_team', is_team);
732 var final_config = {
733 on: {
734 success: function(result) {
735 var team_member = false;
736 var i;
737 for (i=0; i<result.entries.length; i++) {
738 if (result.entries[i].get('member_link') ===
739 Y.lp.client.get_absolute_uri(
740 subscription.get(
741 'subscriber').get('uri'))) {
742 team_member = true;
743 }
744 }
745
746 if (team_member) {
747 subscription.set('can_be_unsubscribed', true);
748 add_temp_user_name(subscription);
749 } else {
750 subscription.set(
751 'can_be_unsubscribed', false);
752 add_temp_user_name(subscription);
753 }
754 },
755
756 failure: error_handler.getFailureHandler()
757 }
758 };
759
760 if (is_team) {
761 // Get a list of members to see if current user
762 // is a team member.
763 var members = result.get(
764 'members_details_collection_link');
765 lp_client.get(members, final_config);
766 } else {
767 subscription.set('can_be_unsubscribed', false);
768 add_temp_user_name(subscription);
769 }
770 },
771
772 failure: error_handler.getFailureHandler()
773 }
774 };
775 var uri = Y.lp.client.get_absolute_uri(
776 subscription.get('person').get('escaped_uri'));
777 lp_client.get(uri, config);
778}
779
780/*
781 * Add a grayed out, temporary user name when subscribing
782 * someone else.
783 *
784 * @method add_temp_user_name
785 * @param subscription_link {Node} The sub/unsub link.
786 */
787function add_temp_user_name(subscription) {
788 // Be paranoid about display_name, since timeouts or other errors
789 // could mean display_name wasn't set on initialization.
790 subscription.get('person').set_display_name(function () {
791 _add_temp_user_name(subscription);
792 });
793}
794
795function _add_temp_user_name(subscription) {
796 var display_name = subscription.get('person').get('display_name');
797 var img_src;
798 if (subscription.is_team()) {
799 img_src = '/@@/teamgray';
800 } else {
801 img_src = '/@@/persongray';
802 }
803
804 // The <span>...</span> below must *not* be <span/>. On FF (maybe
805 // others, but at least on FF 3.0.11) will then not notice any
806 // following sibling nodes, like the spinner image.
807 var link_node = Y.Node.create([
808 '<div id="temp-username"> ',
809 ' <img alt="" width="14" height="14" />',
810 ' <span>Other Display Name</span>',
811 ' <img id="temp-name-spinner" src="/@@/spinner" alt="" ',
812 ' style="position:absolute;right:8px" /></div>'].join(''));
813 link_node.one('img').set('src', img_src);
814 link_node.replaceChild(
815 document.createTextNode(display_name),
816 link_node.one('span'));
817
818 var subscribers = Y.one('#subscribers-links');
819 var next = get_next_subscriber_node(subscription);
820 if (next) {
821 subscribers.insertBefore(link_node, next);
822 } else {
823 // Handle the case of no subscribers.
824 var none_subscribers = Y.one('#none-subscribers');
825 if (none_subscribers) {
826 var none_parent = none_subscribers.get('parentNode');
827 none_parent.removeChild(none_subscribers);
828 }
829 subscribers.appendChild(link_node);
830 }
831
832 // Fire a custom event to know it's safe to begin
833 // any actual subscribing work.
834 namespace.portlet.fire('blueprints:nameloaded', subscription);
835}
836
837/**
838 * Set up and configure the module.
839 */
840 namespace.setup_config = function(config) {
841 if (config.yio !== undefined) {
842 //We can be given an alternative IO provider for use in tests.
843 YIO = config.yio;
844 }
845};
846
847}, "0.1", {"requires": ["base", "oop", "node", "event", "io-base",
848 "json-parse", "substitute", "widget-position-ext",
849 "lazr.formoverlay", "lazr.anim", "lazr.base",
850 "lazr.overlay", "lazr.choiceedit", "lp.app.picker",
851 "lp.client",
852 "lp.client.plugins", "lp.app.subscriber",
853 "lp.app.subscribers_list", "lp.app.errors"]});
0854
=== added directory 'lib/lp/blueprints/javascript/tests'
=== added file 'lib/lp/blueprints/javascript/tests/test_subscription_portlet.html'
--- lib/lp/blueprints/javascript/tests/test_subscription_portlet.html 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/tests/test_subscription_portlet.html 2011-06-16 02:58:38 +0000
@@ -0,0 +1,47 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html>
4 <head>
5 <title>Blueprint subscription portlet</title>
6
7 <!-- YUI 3.0 Setup -->
8 <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
9 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
10 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
11 <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
12 <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" />
13
14 <!-- Some required dependencies -->
15 <script type="text/javascript" src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
16 <script type="text/javascript" src="../../../app/javascript/client.js"></script>
17 <script type="text/javascript" src="../../../app/javascript/errors.js"></script>
18 <script type="text/javascript" src="../../../app/javascript/picker.js"></script>
19 <script type="text/javascript" src="../../../app/javascript/widgets.js"></script>
20 <script type="text/javascript" src="../../../app/javascript/subscriber.js"></script>
21 <script type="text/javascript" src="../../../app/javascript/subscribers_list.js"></script>
22
23 <!-- The module under test -->
24 <script type="text/javascript" src="../blueprint_index_portlets.js"></script>
25
26 <!-- The test suite -->
27 <script type="text/javascript" src="test_subscription_portlet.js"></script>
28</head>
29<body class="yui3-skin-sam">
30 <div id="portlet-subscribers" class="portlet vertical">
31 <div class="section">
32 <div id="sub-unsub-spinner">Subscribing...</div>
33 <a class="menu-link-addsubscriber"
34 href="https://blueprints.launchpad.dev/foo/+spec/bar/+addsubscriber">Subscribe someone else</a>
35 </div>
36 <a id="subscribers-ids-link"
37 href="https://blueprints.launchpad.dev/foo/+spec/bar/+blueprint-portlet-subscribers-ids"></a>
38 <a id="subscribers-content-link"
39 href="https://blueprints.launchpad.dev/foo/+spec/bar/+blueprint-portlet-subscribers-content"></a>
40 <div id="subscribers-portlet-spinner"
41 style="text-align: center; display: none">
42 <img src="/@@/spinner" />
43 </div>
44 </div>
45 <div id="log"></div>
46</body>
47</html>
048
=== added file 'lib/lp/blueprints/javascript/tests/test_subscription_portlet.js'
--- lib/lp/blueprints/javascript/tests/test_subscription_portlet.js 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/javascript/tests/test_subscription_portlet.js 2011-06-16 02:58:38 +0000
@@ -0,0 +1,285 @@
1/* Copyright (c) 2011, Canonical Ltd. All rights reserved. */
2
3YUI({
4 base: '../../../../canonical/launchpad/icing/yui/',
5 filter: 'raw',
6 combine: false,
7 fetchCSS: false
8 }).use('test', 'console', 'node', 'event', 'event-simulate',
9 'lp.client', 'lp.app.subscriber', 'lp.app.subscribers_list',
10 'lp.blueprints.blueprint_index.portlets',
11 function(Y) {
12
13var suite = new Y.Test.Suite("lp.blueprints.subscriber_portlet Tests");
14var module = Y.lp.blueprints.blueprint_index.portlets;
15module.setup_portlet_handlers();
16
17/*
18 * A wrapper for the Y.Event.simulate() function. The wrapper accepts
19 * CSS selectors and Node instances instead of raw nodes.
20 */
21function simulate(widget, selector, evtype, options) {
22 var rawnode = Y.Node.getDOMNode(widget.one(selector));
23 Y.Event.simulate(rawnode, evtype, options);
24}
25
26/**
27 * A stub io handler.
28 */
29function IOStub(test_case){
30 if (!(this instanceof IOStub)) {
31 throw new Error("Constructor called as a function");
32 }
33 this.calls = [];
34 this.responses = [];
35 this.io = function(url, config) {
36 this.calls.push(url);
37 var response = {responseText: ''};
38 // We may have been passed text to use in the response.
39 if (Y.Lang.isValue(arguments.callee.responseText)) {
40 response.responseText = arguments.callee.responseText;
41 } else if (this.responses.length>0) {
42 response.responseText = this.responses.shift();
43 }
44
45 // We currently only support calling the success handler.
46 config.on.success(undefined, response, arguments.callee.args);
47 // After calling the handler, resume the test.
48 if (Y.Lang.isFunction(arguments.callee.doAfter)) {
49 test_case.resume(arguments.callee.doAfter);
50 }
51 };
52}
53
54suite.add(new Y.Test.Case({
55 name: "lp.blueprints.subscriber_portlet",
56
57 load_portlet: function() {
58 var subscription_link = Y.one('.menu-link-subscription');
59 var subscription_link_handler = undefined;
60 // Until edit subscription overlays are supported we don't want to
61 // load the ajax subscribers portal on the html subscription edit
62 // page.
63 var load_subscribers_portlet = true;
64 if (subscription_link) {
65 load_subscribers_portlet = !subscription_link.hasClass('nolink');
66 subscription_link_handler = subscription_link.on(
67 'click', function(e) { e.preventDefault(); });
68 }
69 if (load_subscribers_portlet) {
70 Y.lp.blueprints.blueprint_index.portlets.load_subscribers_portlet(
71 subscription_link, subscription_link_handler);
72 }
73 },
74
75 setUp: function() {
76 // Some common data required for each test.
77 this.MY_WEB_LINK = 'https://blueprints.launchpad.dev/foo/+spec/bar';
78 this.MY_NAME = "ME";
79 this.ME = new Y.lp.client.Entry();
80 this.ME.addAttr('display_name', {value: "ME"});
81 this.SUBSCRIBER_ROW_HTML =
82 '<div class="subscriber" id="subscription-subscriber-12">' +
83 ' <a href="/foo/+spec/bar/+subscription/me">' +
84 ' <img alt="" src="/@@/subscriber-inessential"' +
85 ' title="Normal subscriber." />' +
86 ' </a>' +
87 ' <a href="/~me">ME</a>' +
88 ' <a href="+subscribe" id="unsubscribe-subscriber-12"' +
89 ' title="Unsubscribe ME" class="unsub-icon">' +
90 ' <img></img>' +
91 ' </a>' +
92 '</div>';
93
94 window.LP = {
95 links: { me: "/~" + this.MY_NAME },
96 cache: {
97 context: {
98 self_link: 'https://launchpad.dev/api/devel/foo/+spec/bar/'
99 }
100 }
101 };
102 // A container to allow us to specify what results to return for
103 // named_post and get calls during the test. Each subsequent call
104 // takes its return data from the front of the array.
105 window.expected_results = {
106 get_results: [],
107 named_post_results: []
108 };
109
110 // We need to stub out Launchpad client named_post and get operations
111 // so that we can record what requests were made and provide test data
112 // back to the caller.
113 Y.lp.client.Launchpad = function() {};
114 Y.lp.client.Launchpad.prototype.named_post =
115 function(url, func, config) {
116 LP.cache.named_post_call_data = {
117 called_url: url,
118 called_func: func,
119 called_config: config
120 };
121 // our setup assumes success, so we just do the
122 // success callback.
123 var result = '';
124 if (!Y.Lang.isArray(expected_results.named_post_results)) {
125 result = expected_results.named_post_results;
126 } else {
127 if (expected_results.named_post_results.length > 0) {
128 result = expected_results.named_post_results.shift();
129 }
130 }
131 config.on.success(result);
132 };
133 Y.lp.client.Launchpad.prototype.get =
134 function(url, config) {
135 LP.cache.get_call_data = {
136 called_url: url
137 };
138 var result = '';
139 if (!Y.Lang.isArray(expected_results.get_results)) {
140 result = expected_results.get_results;
141 } else {
142 if (expected_results.get_results.length > 0) {
143 result = expected_results.get_results.shift();
144 }
145 }
146 config.on.success(result);
147 };
148
149 this.root_node = Y.one('#portlet-subscribers');
150 this.config = {};
151 this.config.yio = new IOStub(this);
152 module.setup_config(this.config);
153 },
154
155 removeNode: function(selector) {
156 var node = Y.one(selector);
157 if (node) {
158 node.remove(true);
159 }
160 },
161
162 tearDown: function() {
163 delete window.LP;
164 delete window.expected_results;
165 this.removeNode('#subscribe-node');
166 this.removeNode('#subscribers');
167 this.removeNode('.yui3-overlay');
168 },
169
170 check_portlet_loaded: function() {
171 Y.ArrayAssert.itemsAreEqual(
172 [this.MY_WEB_LINK+'/+blueprint-portlet-subscribers-content',
173 this.MY_WEB_LINK+'/+blueprint-portlet-subscribers-ids'],
174 this.config.yio.calls);
175 },
176
177 /**
178 * Create an initial page state where the current user is either
179 * subscribed or unsubscribed to the blueprint.
180 */
181 setup_me: function(is_subscribed) {
182 var subscribe_section = Y.one(".portlet .section");
183 var subscribe_node = Y.Node.create("<div id='subscribe-node'></div>");
184 subscribe_node.addClass('subscribed-'+is_subscribed);
185 var subscribe_link = Y.Node.create("<a>Subscribe</a>");
186 subscribe_link.addClass('menu-link-subscription');
187 subscribe_node.appendChild(subscribe_link);
188 subscribe_section.prepend(subscribe_node);
189 var subscriber_html='';
190 if (is_subscribed) {
191 subscriber_html = this.SUBSCRIBER_ROW_HTML;
192 }
193 var portlet_content =
194 '<div id="subscribers">' +
195 ' <h2>Subscribers</h2>' +
196 ' <div id="subscribers-links">' +
197 subscriber_html +
198 ' </div>'+
199 '</div>';
200 this.config.yio.responses = [
201 portlet_content, '{"me": "subscriber-12"}'];
202 expected_results.get_results = this.ME;
203 this.load_portlet();
204 this.check_portlet_loaded();
205 },
206
207 /**
208 * Create an initial page state where the current user is unsubscribed to
209 * the blueprint.
210 */
211 setup_me_unsubscribed: function() {
212 this.setup_me(false);
213 },
214
215 /**
216 * Create an initial page state where the current user is subscribed to
217 * the blueprint.
218 */
219 setup_me_subscribed: function() {
220 this.setup_me(true);
221 var subscribers = Y.one('#subscribers-links');
222 Y.Assert.isNotNull(
223 subscribers.one(".subscriber a:nth-child(2)[href='/~me']"));
224 },
225
226 test_subscribe_me: function() {
227 // The Subscribe link works if the user is unsubscribed.
228 this.setup_me_unsubscribed();
229 expected_results.get_results = this.ME;
230 var result = new Y.lp.client.Entry();
231 result.set('self_link',
232 LP.cache.context.self_link+'/+subscription/me');
233 expected_results.named_post_results = [result];
234
235 this.config.yio.responses = [this.SUBSCRIBER_ROW_HTML];
236
237 simulate(this.root_node, '.menu-link-subscription', 'click');
238 Y.Assert.areEqual(LP.cache.context.self_link,
239 LP.cache.named_post_call_data.called_url);
240 Y.Assert.areEqual('subscribe',
241 LP.cache.named_post_call_data.called_func);
242 var subscribers = Y.one('#subscribers-links');
243 Y.Assert.isNotNull(
244 subscribers.one(".subscriber a:nth-child(2)[href='/~me']"));
245 },
246
247 test_edit_my_subscription: function() {
248 // If the user is subscribed, the subscribe link is not ajaxified.
249 this.setup_me_subscribed();
250 var subscribe_node = Y.one('.menu-link-subscription');
251 Y.Assert.isFalse(subscribe_node.hasClass('js-action'));
252 },
253
254 test_unsubscribe_me: function() {
255 // The unsubscribe link works if the user is subscribed.
256 this.setup_me_subscribed();
257 simulate(this.root_node, '.unsub-icon', 'click');
258 Y.Assert.areEqual(LP.cache.context.self_link,
259 LP.cache.named_post_call_data.called_url);
260 Y.Assert.areEqual('unsubscribe',
261 LP.cache.named_post_call_data.called_func);
262 Y.on('contentready', function() {
263 Y.Assert.isNotNull(Y.one("#none-subscribers"));
264 }, '#subscribers');
265 }
266}));
267
268// Lock, stock, and two smoking barrels.
269var handle_complete = function(data) {
270 status_node = Y.Node.create(
271 '<p id="complete">Test status: complete</p>');
272 Y.one('body').appendChild(status_node);
273 };
274Y.Test.Runner.on('complete', handle_complete);
275Y.Test.Runner.add(suite);
276
277var yui_console = new Y.Console({
278 newestOnTop: false
279});
280yui_console.render('#log');
281
282Y.on('domready', function() {
283 Y.Test.Runner.run();
284});
285});
0286
=== modified file 'lib/lp/blueprints/templates/specification-index.pt'
--- lib/lp/blueprints/templates/specification-index.pt 2011-04-14 22:33:16 +0000
+++ lib/lp/blueprints/templates/specification-index.pt 2011-06-16 02:58:38 +0000
@@ -319,8 +319,8 @@
319 </div>319 </div>
320320
321 <script type="text/javascript">321 <script type="text/javascript">
322 LPS.use('lazr.anim', 'lp.ui', function(Y) {322 LPS.use('lazr.anim', 'lp.ui', 'lp.blueprints.blueprint_index', function(Y) {
323323 Y.lp.blueprints.blueprint_index.setup_blueprint_index();
324 Y.on('lp:context:implementation_status:changed', function(e) {324 Y.on('lp:context:implementation_status:changed', function(e) {
325 var icon = Y.one('#informational-icon');325 var icon = Y.one('#informational-icon');
326 if (e.new_value == 'Informational') {326 if (e.new_value == 'Informational') {
327327
=== modified file 'lib/lp/blueprints/templates/specification-portlet-subscribers.pt'
--- lib/lp/blueprints/templates/specification-portlet-subscribers.pt 2011-06-16 02:58:37 +0000
+++ lib/lp/blueprints/templates/specification-portlet-subscribers.pt 2011-06-16 02:58:38 +0000
@@ -5,13 +5,56 @@
5 class="portlet vertical"5 class="portlet vertical"
6 id="portlet-subscribers"6 id="portlet-subscribers"
7 metal:define-macro="custom"7 metal:define-macro="custom"
8 tal:define="features request/features"
8>9>
9 <div class="section" tal:define="context_menu context/menu:context"10 <div class="section" tal:define="context_menu context/menu:context"
10 metal:define-slot="heading">11 metal:define-slot="heading">
11 <div12 <div
12 tal:attributes="class view/current_user_subscription_class"13 tal:attributes="class view/current_user_subscription_class"
13 tal:content="structure context_menu/subscription/render" />14 tal:content="structure context_menu/subscription/render" />
15 <div id="sub-unsub-spinner">Subscribing...</div>
14 <div tal:content="structure context_menu/addsubscriber/render" />16 <div tal:content="structure context_menu/addsubscriber/render" />
15 </div>17 </div>
16 <div tal:replace="structure context/@@+blueprint-portlet-subscribers-content" />18<a id="subscribers-ids-link"
19 tal:define="blueprint context"
20 tal:attributes="href blueprint/fmt:url/+blueprint-portlet-subscribers-ids"></a>
21<a id="subscribers-content-link"
22 tal:define="blueprint context"
23 tal:attributes="href blueprint/fmt:url/+blueprint-portlet-subscribers-content"></a>
24<div id="subscribers-portlet-spinner"
25 style="text-align: center; display: none">
26<img src="/@@/spinner" />
27</div>
28<div tal:condition="features/disclosure.enhanced_blueprint_subscriptions.enabled">
29<script type="text/javascript">
30LPS.use('io-base', 'node', 'lp.blueprints.blueprint_index.portlets', function(Y) {
31 // Must be done inline here to ensure the load event fires.
32 // This is a work around for a YUI3 issue with event handling.
33 var subscription_link = Y.one('.menu-link-subscription');
34 var subscription_link_handler = undefined;
35 // Until edit subscription overlays are supported we don't want to load
36 // the ajax subscribers portal on the html subscription edit page.
37 var load_subscribers_portlet = true;
38 if (subscription_link) {
39 load_subscribers_portlet = !subscription_link.hasClass('nolink');
40 subscription_link_handler = subscription_link.on(
41 'click', function(e) { e.preventDefault(); });
42 }
43
44 if (load_subscribers_portlet) {
45 Y.on('domready', function() {
46 if (Y.lp.blueprints.blueprint_index.portlets) {
47 Y.lp.blueprints.blueprint_index.portlets.load_subscribers_portlet(
48 subscription_link, subscription_link_handler);
49 }
50 });
51 }
52});
53</script>
54<noscript>
55<div tal:replace="structure context/@@+blueprint-portlet-subscribers-content"/>
56</noscript>
57</div>
58<div tal:condition="not: features/disclosure.enhanced_blueprint_subscriptions.enabled"
59 tal:replace="structure context/@@+blueprint-portlet-subscribers-content"/>
17</div>60</div>
1861
=== modified file 'lib/lp/blueprints/templates/specification-subscriber-row.pt'
--- lib/lp/blueprints/templates/specification-subscriber-row.pt 2011-06-16 02:58:37 +0000
+++ lib/lp/blueprints/templates/specification-subscriber-row.pt 2011-06-16 02:58:38 +0000
@@ -37,6 +37,7 @@
37 <a tal:attributes="href subscription/person/fmt:url:blueprints"37 <a tal:attributes="href subscription/person/fmt:url:blueprints"
38 tal:content="subscription/person/fmt:displayname"38 tal:content="subscription/person/fmt:displayname"
39 />39 />
40 <tal:block condition="request/features/disclosure.enhanced_blueprint_subscriptions.enabled">
40 <a tal:define="user request/lp:person"41 <a tal:define="user request/lp:person"
41 tal:condition="python: subscription.canBeUnsubscribedByUser(user)"42 tal:condition="python: subscription.canBeUnsubscribedByUser(user)"
42 href="+subscribe"43 href="+subscribe"
@@ -47,5 +48,6 @@
47 <img class="unsub-icon" src="/@@/remove" alt="Remove"48 <img class="unsub-icon" src="/@@/remove" alt="Remove"
48 tal:attributes="id string:unsubscribe-icon-${subscription/css_name}" />49 tal:attributes="id string:unsubscribe-icon-${subscription/css_name}" />
49 </a>50 </a>
51 </tal:block>
50 </div>52 </div>
51</tal:root>53</tal:root>
5254
=== modified file 'lib/lp/services/features/flags.py'
--- lib/lp/services/features/flags.py 2011-06-11 03:42:08 +0000
+++ lib/lp/services/features/flags.py 2011-06-16 02:58:38 +0000
@@ -114,6 +114,10 @@
114 'boolean',114 'boolean',
115 ('Enables ranking by pillar affiliation in the person picker.'),115 ('Enables ranking by pillar affiliation in the person picker.'),
116 ''),116 ''),
117 ('disclosure.enhanced_blueprint_subscriptions.enabled',
118 'boolean',
119 ('Enables improved blueprint subscription features.'),
120 ''),
117 ])121 ])
118122
119# The set of all flag names that are documented.123# The set of all flag names that are documented.