Merge lp:~danilo/launchpad/bug-772754-other-subscribers-actions into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Merged at revision: 13243
Proposed branch: lp:~danilo/launchpad/bug-772754-other-subscribers-actions
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/bug-772754-other-subscribers-loading
Diff against target: 912 lines (+680/-17)
7 files modified
lib/lp/bugs/browser/bugsubscription.py (+8/-1)
lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+11/-1)
lib/lp/bugs/javascript/bugtask_index_portlets.js (+0/-1)
lib/lp/bugs/javascript/subscribers_list.js (+172/-9)
lib/lp/bugs/javascript/tests/test_subscribers_list.html (+4/-0)
lib/lp/bugs/javascript/tests/test_subscribers_list.js (+483/-4)
lib/lp/bugs/templates/bugtask-index.pt (+2/-1)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-772754-other-subscribers-actions
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+64187@code.launchpad.net

Description of the change

= Bug 772754: Other subscribers list, part 6 =

Warning: slightly over-sized as well, mostly for the long JS tests which emulate lp_client behaviour.

This is part of ongoing work for providing the "other subscribers" list as indicated in mockup https://launchpadlibrarian.net/71552495/all-in-one.png attached to bug 772754 by Gary.

Provides subscribe-someone-else and unsubscribe actions that actually work with Launchpad.

Existing list of subscribers is only removed in the next branch, so with this branch, you have two lists of which only this one will now really work.

== Tests ==

lp/bugs/javascript/tests/test_subscribers_list.html

bin/test -cvvt BugPortletSubscribersWithDetailsTests

== Demo and Q/A ==

N/A

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bugsubscription.py
  lib/lp/bugs/browser/tests/test_bugsubscription_views.py
  lib/lp/bugs/javascript/bugtask_index_portlets.js
  lib/lp/bugs/javascript/subscribers_list.js
  lib/lp/bugs/javascript/tests/test_subscribers_list.html
  lib/lp/bugs/javascript/tests/test_subscribers_list.js
  lib/lp/bugs/templates/bugtask-index.pt

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Danilo this branch looks good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
2--- lib/lp/bugs/browser/bugsubscription.py 2011-06-15 11:04:53 +0000
3+++ lib/lp/bugs/browser/bugsubscription.py 2011-06-15 11:05:00 +0000
4@@ -16,7 +16,10 @@
5 import cgi
6
7 from lazr.delegates import delegates
8-from lazr.restful.interfaces import IJSONRequestCache
9+from lazr.restful.interfaces import (
10+ IJSONRequestCache,
11+ IWebServiceClientRequest,
12+)
13 from simplejson import dumps
14 from zope import formlib
15 from zope.app.form import CustomWidgetFactory
16@@ -27,6 +30,7 @@
17 SimpleVocabulary,
18 )
19 from zope.security.proxy import removeSecurityProxy
20+from zope.traversing.browser import absoluteURL
21
22 from canonical.launchpad import _
23 from canonical.launchpad.webapp import (
24@@ -590,6 +594,7 @@
25 """Return subscriber_ids in a form suitable for JavaScript use."""
26 data = []
27 details = list(self.context.getDirectSubscribersWithDetails())
28+ api_request = IWebServiceClientRequest(self.request)
29 for person, subscription in details:
30 if person == self.user:
31 # Skip the current user viewing the page.
32@@ -599,6 +604,7 @@
33 'name': person.name,
34 'display_name': person.displayname,
35 'web_link': canonical_url(person, rootsite='mainsite'),
36+ 'self_link': absoluteURL(person, api_request),
37 'is_team': person.is_team,
38 'can_edit': can_edit,
39 }
40@@ -618,6 +624,7 @@
41 'name': person.name,
42 'display_name': person.displayname,
43 'web_link': canonical_url(person, rootsite='mainsite'),
44+ 'self_link': absoluteURL(person, api_request),
45 'is_team': person.is_team,
46 'can_edit': False,
47 }
48
49=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
50--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-15 11:04:53 +0000
51+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-06-15 11:05:00 +0000
52@@ -7,10 +7,12 @@
53
54 from simplejson import dumps
55
56+from zope.traversing.browser import absoluteURL
57+
58 from canonical.launchpad.ftests import LaunchpadFormHarness
59 from canonical.launchpad.webapp import canonical_url
60 from canonical.testing.layers import LaunchpadFunctionalLayer
61-
62+from lazr.restful.interfaces import IWebServiceClientRequest
63 from lp.bugs.browser.bugsubscription import (
64 BugPortletSubcribersIds,
65 BugPortletSubscribersWithDetails,
66@@ -523,6 +525,7 @@
67 bug.subscribe(subscriber, subscriber,
68 level=BugNotificationLevel.LIFECYCLE)
69 harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
70+ api_request = IWebServiceClientRequest(harness.request)
71
72 expected_result = {
73 'subscriber': {
74@@ -531,6 +534,7 @@
75 'is_team': False,
76 'can_edit': False,
77 'web_link': canonical_url(subscriber),
78+ 'self_link': absoluteURL(subscriber, api_request),
79 },
80 'subscription_level': "Lifecycle",
81 }
82@@ -547,6 +551,7 @@
83 bug.subscribe(subscriber, subscriber.teamowner,
84 level=BugNotificationLevel.LIFECYCLE)
85 harness = LaunchpadFormHarness(bug, BugPortletSubscribersWithDetails)
86+ api_request = IWebServiceClientRequest(harness.request)
87
88 expected_result = {
89 'subscriber': {
90@@ -555,6 +560,7 @@
91 'is_team': True,
92 'can_edit': False,
93 'web_link': canonical_url(subscriber),
94+ 'self_link': absoluteURL(subscriber, api_request),
95 },
96 'subscription_level': "Lifecycle",
97 }
98@@ -572,6 +578,7 @@
99 level=BugNotificationLevel.LIFECYCLE)
100 harness = LaunchpadFormHarness(
101 bug, BugPortletSubscribersWithDetails)
102+ api_request = IWebServiceClientRequest(harness.request)
103
104 expected_result = {
105 'subscriber': {
106@@ -580,6 +587,7 @@
107 'is_team': True,
108 'can_edit': True,
109 'web_link': canonical_url(subscriber),
110+ 'self_link': absoluteURL(subscriber, api_request),
111 },
112 'subscription_level': "Lifecycle",
113 }
114@@ -599,6 +607,7 @@
115 level=BugNotificationLevel.LIFECYCLE)
116 harness = LaunchpadFormHarness(
117 bug, BugPortletSubscribersWithDetails)
118+ api_request = IWebServiceClientRequest(harness.request)
119
120 expected_result = {
121 'subscriber': {
122@@ -607,6 +616,7 @@
123 'is_team': True,
124 'can_edit': True,
125 'web_link': canonical_url(subscriber),
126+ 'self_link': absoluteURL(subscriber, api_request),
127 },
128 'subscription_level': "Lifecycle",
129 }
130
131=== modified file 'lib/lp/bugs/javascript/bugtask_index_portlets.js'
132--- lib/lp/bugs/javascript/bugtask_index_portlets.js 2011-06-15 11:04:53 +0000
133+++ lib/lp/bugs/javascript/bugtask_index_portlets.js 2011-06-15 11:05:00 +0000
134@@ -309,7 +309,6 @@
135
136 var subscription = get_subscribe_self_subscription();
137
138- setup_subscribe_someone_else_handler(subscription);
139 }
140
141 function load_subscribers_from_duplicates() {
142
143=== modified file 'lib/lp/bugs/javascript/subscribers_list.js'
144--- lib/lp/bugs/javascript/subscribers_list.js 2011-06-15 11:04:53 +0000
145+++ lib/lp/bugs/javascript/subscribers_list.js 2011-06-15 11:05:00 +0000
146@@ -1,7 +1,23 @@
147 /* Copyright 2011 Canonical Ltd. This software is licensed under the
148 * GNU Affero General Public License version 3 (see the file LICENSE).
149 *
150- * Functions for managing the subscribers list.
151+ * Classes for managing the subscribers list.
152+ *
153+ * Two classes are provided:
154+ *
155+ * - SubscribersList: deals with node construction/removal for the
156+ * list of subscribers, including activity indication and animations.
157+ *
158+ * Public methods to use:
159+ * startActivity, stopActivity,
160+ * addSubscriber, removeSubscriber, indicateSubscriberActivity,
161+ * stopSubscriberActivity, addUnsubscribeAction
162+ *
163+ * - BugSubscribersLoader: loads subscribers from LP, allows subscribing
164+ * someone else and sets unsubscribe actions where appropriate.
165+ * Depends on the SubscribersList to do the actual node construction.
166+ *
167+ * No public methods are available: it all gets run from the constructor.
168 *
169 * @module bugs
170 * @submodule subscribers_list
171@@ -137,6 +153,7 @@
172 */
173 function BugSubscribersLoader(config) {
174 var sl = this.subscribers_list = new SubscribersList(config);
175+
176 if (!Y.Lang.isValue(config.bug) ||
177 !Y.Lang.isString(config.bug.web_link)) {
178 Y.error(
179@@ -144,6 +161,7 @@
180 }
181 this.bug = config.bug;
182
183+ // Get BugSubscribersWithDetails portlet link to load subscribers from.
184 if (!Y.Lang.isString(config.subscribers_details_view)) {
185 Y.error(
186 "No config.subscribers_details_view specified to load " +
187@@ -165,6 +183,12 @@
188 }
189
190 this._loadSubscribers();
191+
192+ // Check for CSS class for the link to subscribe someone else.
193+ if (Y.Lang.isString(config.subscribe_someone_else_link)) {
194+ this.subscribe_someone_else_link = config.subscribe_someone_else_link;
195+ this._setupSubscribeSomeoneElse();
196+ }
197 }
198 namespace.BugSubscribersLoader = BugSubscribersLoader;
199
200@@ -268,19 +292,157 @@
201 * Return a function object that accepts SubscribersList and subscriber
202 * objects as parameters.
203 *
204+ * Constructed function tries to unsubscribe subscriber from the
205+ * this.bug, and indicates activity in the subscribers list.
206+ *
207 * @method _getUnsubscribeCallback
208 */
209 BugSubscribersLoader.prototype._getUnsubscribeCallback = function() {
210+ var loader = this;
211 return function(subscribers_list, subscriber) {
212- subscribers_list.indicateSubscriberActivity(subscriber);
213- // Simulated unsubscribing action for prototyping the UI.
214- setTimeout(function() {
215+ function on_success() {
216 subscribers_list.stopSubscriberActivity(
217 subscriber, true, function() {
218- subscribers_list.removeSubscriber(subscriber);
219- });
220- }, 2400);
221- };
222+ subscribers_list.removeSubscriber(subscriber);
223+ });
224+ }
225+ function on_failure(t_id, response) {
226+ subscribers_list.stopSubscriberActivity(subscriber, false);
227+ Y.lp.app.errors.display_error(
228+ false,
229+ response.status + " (" + response.statusText + ")."
230+ );
231+ }
232+
233+ var config = {
234+ on: { success: on_success,
235+ failure: on_failure },
236+ parameters: { person: subscriber.self_link }
237+ };
238+ subscribers_list.indicateSubscriberActivity(subscriber);
239+ loader.lp_client.named_post(
240+ loader.bug.self_link, 'unsubscribe', config);
241+ };
242+};
243+
244+/**
245+ * Set-up subscribe-someone-else link to pop-up a picker and subscribe
246+ * the selected person/team.
247+ *
248+ * On `save' from the picker, fetch the actual person object via API
249+ * and pass it into _subscribeSomeoneElse().
250+ *
251+ * @method _setupSubscribeSomeoneElse
252+ */
253+BugSubscribersLoader.prototype._setupSubscribeSomeoneElse = function() {
254+ var loader = this;
255+ var config = {
256+ header: 'Subscribe someone else',
257+ step_title: 'Search',
258+ picker_activator: this.subscribe_someone_else_link
259+ };
260+ if (Y.one(this.subscribe_someone_else_link) === null) {
261+ Y.error("No link matching CSS selector '" +
262+ this.subscribe_someone_else_link +
263+ "' for subscribing someone else found.");
264+ }
265+ config.save = function(result) {
266+ var person_uri = Y.lp.client.get_absolute_uri(result.api_uri);
267+ loader.lp_client.get(person_uri, {
268+ on: {
269+ success: function(person) {
270+ loader._subscribeSomeoneElse(person);
271+ },
272+ failure: function(t_id, response) {
273+ Y.lp.app.errors.display_error(
274+ false,
275+ response.status + " (" + response.statusText + ")\n" +
276+ "Couldn't get subscriber details from the " +
277+ "server, so they have not been subscribed.\n"
278+ );
279+ }
280+ } });
281+ };
282+ // We store the picker for testing only.
283+ this._picker = Y.lp.app.picker.create('ValidPersonOrTeam', config);
284+};
285+
286+/**
287+ * Subscribe a person or a team to the bug.
288+ *
289+ * This is a callback for the subscribe someone else picker.
290+ *
291+ * @method _subscribeSomeoneElse
292+ * @param person {Object} Representation of a person returned by the API.
293+ * It's an object that returns all attributes with getAttrs() method.
294+ * Must have at least self_link attribute which is passed as
295+ * a parameter to the API 'unsubscribe' call.
296+ */
297+BugSubscribersLoader.prototype._subscribeSomeoneElse = function(person) {
298+ var subscriber = person.getAttrs();
299+ this.subscribers_list.addSubscriber(subscriber, 'Discussion');
300+ this.subscribers_list.indicateSubscriberActivity(subscriber);
301+
302+ var loader = this;
303+
304+ function on_success() {
305+ loader.subscribers_list.stopSubscriberActivity(subscriber, true);
306+ loader._addUnsubscribeLinkIfTeamMember(subscriber);
307+ }
308+ function on_failure(t_id, response) {
309+ loader.subscribers_list.stopSubscriberActivity(
310+ subscriber, false, function() {
311+ loader.subscribers_list.removeSubscriber(subscriber);
312+ }
313+ );
314+ Y.lp.app.errors.display_error(
315+ false,
316+ response.status + " (" + response.statusText + "). " +
317+ "Failed to subscribe " + subscriber.display_name + "."
318+ );
319+ }
320+ var config = {
321+ on: { success: on_success,
322+ failure: on_failure },
323+ parameters: { person: subscriber.self_link } };
324+ this.lp_client.named_post(this.bug.self_link, 'subscribe', config);
325+};
326+
327+/**
328+ * Add unsubscribe link for a team if the currently logged in user
329+ * is member of the team.
330+ *
331+ * @method _addUnsubscribeLinkIfTeamMember
332+ * @param team {Object} A person object as returned via API.
333+ */
334+BugSubscribersLoader.prototype
335+._addUnsubscribeLinkIfTeamMember = function(team) {
336+ var loader = this;
337+ function on_success(members) {
338+ var team_member = false;
339+ var i;
340+ for (i=0; i<members.entries.length; i++) {
341+ if (members.entries[i].get('member_link') ===
342+ Y.lp.client.get_absolute_uri(LP.links.me)) {
343+ team_member = true;
344+ break;
345+ }
346+ }
347+ if (team_member === true) {
348+ // Add unsubscribe action for the team member.
349+ loader.subscribers_list.addUnsubscribeAction(
350+ team, loader._getUnsubscribeCallback());
351+ }
352+ }
353+
354+ if (Y.Lang.isString(LP.links.me) && team.is_team) {
355+ var config = {
356+ on: { success: on_success }
357+ };
358+
359+ var members_link = team.members_details_collection_link;
360+ this.lp_client.get(members_link, config);
361+ }
362 };
363
364
365@@ -902,4 +1064,5 @@
366 };
367
368
369-}, "0.1", {"requires": ["node", "lazr.anim", "lp.client", "lp.names"]});
370+}, "0.1", {"requires": ["node", "lazr.anim", "lp.app.picker", "lp.app.errors",
371+ "lp.client", "lp.names"]});
372
373=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.html'
374--- lib/lp/bugs/javascript/tests/test_subscribers_list.html 2011-06-15 11:04:53 +0000
375+++ lib/lp/bugs/javascript/tests/test_subscribers_list.html 2011-06-15 11:05:00 +0000
376@@ -22,6 +22,10 @@
377 src="../../../app/javascript/errors.js"></script>
378 <script type="text/javascript"
379 src="../../../app/javascript/lp-names.js"></script>
380+ <script type="text/javascript"
381+ src="../../../app/javascript/picker.js"></script>
382+ <script type="text/javascript"
383+ src="../../../app/javascript/widgets.js"></script>
384
385 <!-- Pre-requisite -->
386 <script type="text/javascript"
387
388=== modified file 'lib/lp/bugs/javascript/tests/test_subscribers_list.js'
389--- lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-15 11:04:53 +0000
390+++ lib/lp/bugs/javascript/tests/test_subscribers_list.js 2011-06-15 11:05:00 +0000
391@@ -1,9 +1,9 @@
392 YUI({
393 base: '../../../../canonical/launchpad/icing/yui/',
394 filter: 'raw', combine: false, fetchCSS: false
395- }).use('test', 'console', 'lp.bugs.subscriber',
396- 'lp.bugs.subscribers_list', 'node-event-simulate',
397- function(Y) {
398+}).use('test', 'console', 'lp.app.picker', 'lp.bugs.subscriber',
399+ 'lp.bugs.subscribers_list', 'node-event-simulate',
400+ function(Y) {
401
402 var suite = new Y.Test.Suite("lp.bugs.subscribers_list Tests");
403 var module = Y.lp.bugs.subscribers_list;
404@@ -1596,7 +1596,7 @@
405 container_box: '#other-subscribers-container'
406 };
407 if (barebone !== true) {
408- container_config.bug = { web_link: '/base' };
409+ container_config.bug = { web_link: '/base', self_link: '/bug/1' };
410 container_config.subscribers_details_view = '/+details';
411 }
412 root_node.appendChild(node);
413@@ -1937,6 +1937,485 @@
414 }));
415
416
417+
418+/**
419+ * Test BugSubscribersLoader unsubscribe callback function.
420+ */
421+suite.add(new Y.Test.Case({
422+ name: 'BugSubscribersLoader() subscribers loading test',
423+
424+ setUp: function() {
425+ this.root = Y.Node.create('<div />');
426+ Y.one('body').appendChild(this.root);
427+ },
428+
429+ tearDown: function() {
430+ this.root.remove();
431+ },
432+
433+ test_unsubscribe_callback_success: function() {
434+ // _getUnsubscribeCallback returns a function which takes
435+ // subscribers list and subscriber as the two parameters.
436+ // That function calls 'unsubscribe' API method on the bug
437+ // to unsubscribe the user, and on successful completion,
438+ // it removes the user from the subscribers list.
439+
440+ // Mock LP client.
441+ var received_uri, received_method, received_params;
442+ var config = {};
443+ config.lp_client = {
444+ named_post: function(uri, method, my_conf) {
445+ received_uri = uri;
446+ received_method = method;
447+ received_params = my_conf.parameters;
448+ my_conf.on.success();
449+ },
450+ get: function() {}
451+ };
452+ var subscriber = { name: "user", "can_edit": true,
453+ self_link: "user-self-link" };
454+
455+ // Mock removeSubscriber method to ensure it's called.
456+ var removed_subscriber = false;
457+ var old_rmSub = module.SubscribersList.prototype.removeSubscriber;
458+ module.SubscribersList.prototype.removeSubscriber = function(
459+ my_subscriber) {
460+ Y.Assert.areSame(subscriber.name, my_subscriber.name);
461+ removed_subscriber = true;
462+ };
463+
464+ var loader = setUpLoader(this.root, config);
465+ var unsub_callback = loader._getUnsubscribeCallback();
466+ loader._addSubscriber(subscriber);
467+ unsub_callback(loader.subscribers_list, subscriber);
468+
469+ Y.Assert.areSame(loader.bug.self_link, received_uri);
470+ Y.Assert.areSame('unsubscribe', received_method);
471+ Y.Assert.areSame(subscriber.self_link, received_params.person);
472+
473+ this.wait(function() {
474+ // Removal is triggered from the stopSubscriberActivity,
475+ // which shows the success animation first.
476+ Y.Assert.isTrue(removed_subscriber);
477+ }, 1100);
478+
479+ // Restore the real method.
480+ module.SubscribersList.prototype.removeSubscriber = old_rmSub;
481+ },
482+
483+ test_unsubscribe_callback_failure: function() {
484+ // Function returned by _getUnsubscribeCallback calls
485+ // 'unsubscribe' API method on the bug, and on failure,
486+ // it keeps the user in the list and calls
487+ // stopSubscriberActivity to indicate the failure.
488+
489+ // Mock LP client.
490+ var config = {};
491+ config.lp_client = {
492+ named_post: function(uri, method, my_conf) {
493+ my_conf.on.failure(0, { status: 500, statusText: "BOOM!" });
494+ },
495+ get: function() {}
496+ };
497+ var subscriber = { name: "user", "can_edit": true,
498+ self_link: "user-self-link" };
499+
500+ // Mock stopSubscriberActivity to ensure it's called.
501+ var subscriber_activity_stopped = false;
502+ var old_method =
503+ module.SubscribersList.prototype.stopSubscriberActivity;
504+ module.SubscribersList.prototype.stopSubscriberActivity = function(
505+ my_subscriber, success, callback) {
506+ Y.Assert.areSame(subscriber.name, my_subscriber.name);
507+ // The passed-in parameter indicates failure.
508+ Y.Assert.isFalse(success);
509+ // And there is no callback.
510+ Y.Assert.isUndefined(callback);
511+ subscriber_activity_stopped = true;
512+ };
513+
514+ // Ensure display_error is called.
515+ var error_shown = false;
516+ var old_error_method = Y.lp.app.errors.display_error;
517+ Y.lp.app.errors.display_error = function(text) {
518+ error_shown = true;
519+ };
520+
521+ var loader = setUpLoader(this.root, config);
522+ var unsub_callback = loader._getUnsubscribeCallback();
523+ loader._addSubscriber(subscriber);
524+ unsub_callback(loader.subscribers_list, subscriber);
525+
526+ Y.Assert.isTrue(subscriber_activity_stopped);
527+ Y.Assert.isTrue(error_shown);
528+
529+ // Restore original methods.
530+ module.SubscribersList.prototype.stopSubscriberActivity = old_method;
531+ Y.lp.app.errors.display_error = old_error_method;
532+ }
533+
534+}));
535+
536+
537+
538+/**
539+ * Test BugSubscribersLoader subscribe-someone-else functionality.
540+ */
541+suite.add(new Y.Test.Case({
542+ name: 'BugSubscribersLoader() subscribe-someone-else test',
543+
544+ _should: {
545+ error: {
546+ test_setupSubscribeSomeoneElse_error:
547+ new Error("No link matching CSS selector " +
548+ "'#sub-someone-else-link' " +
549+ "for subscribing someone else found.")
550+ }
551+ },
552+
553+ setUp: function() {
554+ this.root = Y.Node.create('<div />');
555+ Y.one('body').appendChild(this.root);
556+ },
557+
558+ tearDown: function() {
559+ this.root.remove();
560+ },
561+
562+ test_constructor_calls_setup: function() {
563+ // When subscribe_someone_else_link is passed in the constructor,
564+ // link identified by that CSS selector is set to pop up a person
565+ // picker for choosing a person/team to subscribe to the bug.
566+ var config = {
567+ subscribe_someone_else_link: '#sub-someone-else-link'
568+ };
569+
570+ var setup_called = false;
571+ // Replace the original method to ensure it's getting called.
572+ var old_method =
573+ module.BugSubscribersLoader.prototype._setupSubscribeSomeoneElse;
574+ module.BugSubscribersLoader.prototype._setupSubscribeSomeoneElse =
575+ function() {
576+ setup_called = true;
577+ };
578+
579+ var loader = setUpLoader(this.root, config);
580+
581+ Y.Assert.isTrue(setup_called);
582+
583+ // Restore original method.
584+ module.BugSubscribersLoader.prototype._setupSubscribeSomeoneElse =
585+ old_method;
586+ },
587+
588+ test_setupSubscribeSomeoneElse_error: function() {
589+ // When link is not found in the page, exception is raised.
590+
591+ // Initialize the loader with no subscribe-someone-else link.
592+ var loader = setUpLoader(this.root);
593+ loader.subscribe_someone_else_link = '#sub-someone-else-link';
594+ loader._setupSubscribeSomeoneElse();
595+ },
596+
597+ test_setupSubscribeSomeoneElse: function() {
598+ // _setupSubscribeSomeoneElse ties in a link with
599+ // the appropriate person picker and with the save
600+ // handler that calls _subscribeSomeoneElse with the
601+ // selected person as the parameter.
602+
603+ // Initialize the loader with no subscribe-someone-else link.
604+ var loader = setUpLoader(this.root);
605+
606+ // Mock LP client that always returns a person-like object.
607+ var subscriber = { name: "user", "can_edit": true,
608+ self_link: "/~user",
609+ api_uri: "/~user" };
610+ loader.lp_client = {
611+ get: function(uri, conf) {
612+ conf.on.success(subscriber);
613+ }
614+ };
615+
616+ loader.subscribe_someone_else_link = '#sub-someone-else-link';
617+ var link = Y.Node.create('<a />').set('id', 'sub-someone-else-link');
618+ this.root.appendChild(link);
619+
620+ // Mock subscribeSomeoneElse method to ensure it's called.
621+ var subscribe_done = false;
622+ var old_method =
623+ module.BugSubscribersLoader.prototype._subscribeSomeoneElse;
624+ module.BugSubscribersLoader.prototype._subscribeSomeoneElse =
625+ function(person) {
626+ Y.Assert.areSame(subscriber, person);
627+ subscribe_done = true;
628+ };
629+
630+ // Mock the picker creation as well.
631+ var picker_shown = false;
632+ var old_create_picker = Y.lp.app.picker.create;
633+ Y.lp.app.picker.create = function(vocabulary, my_config) {
634+ Y.Assert.areSame('ValidPersonOrTeam', vocabulary);
635+ // On link click, simulate the save action.
636+ link.on('click', function() {
637+ picker_shown = true;
638+ my_config.save(subscriber);
639+ });
640+ };
641+
642+ loader._setupSubscribeSomeoneElse();
643+
644+ // Show the picker and simulate the save action.
645+ link.simulate('click');
646+
647+ Y.Assert.isTrue(picker_shown);
648+ Y.Assert.isTrue(subscribe_done);
649+
650+ // Restore original methods.
651+ module.BugSubscribersLoader.prototype._subscribeSomeoneElse =
652+ old_method;
653+ Y.lp.app.picker.create = old_create_picker;
654+ },
655+
656+ test_setupSubscribeSomeoneElse_failure: function() {
657+ // When fetching a person as returned by the picker fails
658+ // error message is shown.
659+
660+ // Initialize the loader with no subscribe-someone-else link.
661+ var loader = setUpLoader(this.root);
662+
663+ // Mock LP client that always returns a person-like object.
664+ var subscriber = { name: "user", "can_edit": true,
665+ self_link: "/~user",
666+ api_uri: "/~user" };
667+ loader.lp_client = {
668+ get: function(uri, conf) {
669+ conf.on.failure(99, { status: 500,
670+ statusText: "BOOM" });
671+ }
672+ };
673+ var expected_error_msg = "500 (BOOM)\n" +
674+ "Couldn't get subscriber details from the " +
675+ "server, so they have not been subscribed.\n";
676+ var received_error_msg;
677+
678+ // Mock display_error to ensure it's called.
679+ var old_display_error = Y.lp.app.errors.display_error;
680+ Y.lp.app.errors.display_error = function(animate, msg) {
681+ Y.Assert.isFalse(animate);
682+ received_error_msg = msg;
683+ };
684+
685+ loader.subscribe_someone_else_link = '#sub-someone-else-link';
686+ var link = Y.Node.create('<a />').set('id', 'sub-someone-else-link');
687+ this.root.appendChild(link);
688+
689+ // Mock the picker creation as well.
690+ var old_create_picker = Y.lp.app.picker.create;
691+ Y.lp.app.picker.create = function(vocabulary, my_config) {
692+ Y.Assert.areSame('ValidPersonOrTeam', vocabulary);
693+ // On link click, simulate the save action.
694+ link.on('click', function() {
695+ my_config.save(subscriber);
696+ });
697+ };
698+
699+ loader._setupSubscribeSomeoneElse();
700+
701+ // Show the picker and simulate the save action.
702+ link.simulate('click');
703+
704+ // display_error was called with the appropriate error message.
705+ Y.Assert.areSame(expected_error_msg, received_error_msg);
706+
707+ // Restore original methods.
708+ Y.lp.app.errors.display_error = old_display_error;
709+ Y.lp.app.picker.create = old_create_picker;
710+ },
711+
712+ test_subscribeSomeoneElse: function() {
713+ // _subscribeSomeoneElse method takes a Person object as returned
714+ // by the API, and adds that subscriber at 'Discussion' level.
715+
716+ var subscriber = { self_link: "/~user" };
717+
718+ // Mock-up addSubscriber method to ensure subscriber is added.
719+ var subscriber_added = false;
720+ var old_addSub = module.SubscribersList.prototype.addSubscriber;
721+ module.SubscribersList.prototype.addSubscriber = function(
722+ my_subscriber, level) {
723+ Y.Assert.areSame(subscriber, my_subscriber);
724+ subscriber_added = true;
725+ };
726+
727+ // Mock-up indicateSubscriberActivity to ensure it's called.
728+ var activity_on = false;
729+ var old_indicate =
730+ module.SubscribersList.prototype.indicateSubscriberActivity;
731+ module.SubscribersList.prototype.indicateSubscriberActivity =
732+ function(my_subscriber) {
733+ Y.Assert.areSame(subscriber, my_subscriber);
734+ activity_on = true;
735+ };
736+
737+ // Initialize the loader.
738+ var loader = setUpLoader(this.root);
739+
740+ // Mock lp_client which records the call.
741+ var received_method, received_uri, received_params;
742+ loader.lp_client = {
743+ named_post: function(uri, method, conf) {
744+ received_uri = uri;
745+ received_method = method;
746+ received_params = conf.parameters;
747+ }
748+ };
749+
750+ // Wrap subscriber like an API-returned value.
751+ var person = {
752+ getAttrs: function() {
753+ return subscriber;
754+ }
755+ };
756+
757+ loader._subscribeSomeoneElse(person);
758+
759+ Y.Assert.isTrue(subscriber_added);
760+ Y.Assert.isTrue(activity_on);
761+
762+ Y.Assert.areEqual('subscribe', received_method);
763+ Y.Assert.areEqual(loader.bug.self_link, received_uri);
764+ Y.Assert.areEqual(subscriber.self_link, received_params.person);
765+
766+ // Restore original methods.
767+ module.SubscribersList.prototype.addSubscriber = old_addSub;
768+ module.SubscribersList.prototype.indicateSubscriberActivity =
769+ old_indicate;
770+ },
771+
772+ test_subscribeSomeoneElse_success: function() {
773+ // When subscribing someone else succeeds, stopSubscriberActivity
774+ // is called indicating success, and _addUnsubscribeLinkIfTeamMember
775+ // is called to add unsubscribe-link when needed.
776+
777+ var subscriber = { name: "user", self_link: "/~user" };
778+
779+ // Initialize the loader.
780+ var loader = setUpLoader(this.root);
781+ loader.subscribers_list.addSubscriber(subscriber, "Maybe");
782+
783+ // Mock-up addUnsubscribeLinkIfTeamMember method to
784+ // ensure it's called with the right parameters.
785+ var subscriber_link_added = false;
786+ var old_addLink =
787+ module.BugSubscribersLoader
788+ .prototype._addUnsubscribeLinkIfTeamMember;
789+ module.BugSubscribersLoader.prototype
790+ ._addUnsubscribeLinkIfTeamMember =
791+ function(my_subscriber) {
792+ Y.Assert.areSame(subscriber, my_subscriber);
793+ subscriber_link_added = true;
794+ };
795+
796+ // Mock-up stopSubscriberActivity to ensure it's called.
797+ var activity_on = true;
798+ var old_indicate =
799+ module.SubscribersList.prototype.stopSubscriberActivity;
800+ module.SubscribersList.prototype.stopSubscriberActivity =
801+ function(my_subscriber, success) {
802+ Y.Assert.areSame(subscriber, my_subscriber);
803+ Y.Assert.isTrue(success);
804+ activity_on = false;
805+ };
806+
807+ // Mock lp_client which calls the success handler.
808+ loader.lp_client = {
809+ named_post: function(uri, method, conf) {
810+ conf.on.success();
811+ }
812+ };
813+
814+ // Wrap subscriber like an API-returned value.
815+ var person = {
816+ getAttrs: function() {
817+ return subscriber;
818+ }
819+ };
820+
821+ loader._subscribeSomeoneElse(person);
822+
823+ Y.Assert.isTrue(subscriber_link_added);
824+ Y.Assert.isFalse(activity_on);
825+
826+ // Restore original methods.
827+ module.BugSubscribersLoader.prototype.addUnsubscribeLinkIfTeamMember =
828+ old_addLink;
829+ module.SubscribersList.prototype.stopSubscriberActivity =
830+ old_indicate;
831+
832+ },
833+
834+ test_subscribeSomeoneElse_failure: function() {
835+ // When subscribing someone else fails, stopSubscriberActivity
836+ // is called indicating failure and it calls removeSubscriber
837+ // from the callback when animation completes.
838+ // Error is shown as well.
839+
840+ var subscriber = { name: "user", self_link: "/~user",
841+ display_name: "User Name" };
842+
843+ // Initialize the loader.
844+ var loader = setUpLoader(this.root);
845+ loader.subscribers_list.addSubscriber(subscriber, "Maybe");
846+
847+ // Mock-up removeSubscriber to ensure it's called.
848+ var remove_called = false;
849+ var old_remove =
850+ module.SubscribersList.prototype.removeSubscriber;
851+ module.SubscribersList.prototype.removeSubscriber =
852+ function(my_subscriber) {
853+ Y.Assert.areSame(subscriber, my_subscriber);
854+ remove_called = true;
855+ };
856+
857+ // Ensure display_error is called.
858+ var old_error_method = Y.lp.app.errors.display_error;
859+ var received_error;
860+ Y.lp.app.errors.display_error = function(anim, text) {
861+ received_error = text;
862+ };
863+
864+ // Mock lp_client which calls the failure handler.
865+ loader.lp_client = {
866+ named_post: function(uri, method, conf) {
867+ conf.on.failure(99, { status: 500,
868+ statusText: "BOOM" });
869+ }
870+ };
871+
872+ // Wrap subscriber like an API-returned value.
873+ var person = {
874+ getAttrs: function() {
875+ return subscriber;
876+ }
877+ };
878+
879+ loader._subscribeSomeoneElse(person);
880+
881+ Y.Assert.areSame('500 (BOOM). Failed to subscribe User Name.',
882+ received_error);
883+
884+ // Remove function is only called after animation completes.
885+ this.wait(function() {
886+ Y.Assert.isTrue(remove_called);
887+ }, 1100);
888+
889+ // Restore original methods.
890+ module.SubscribersList.prototype.removeSubscriber = old_remove;
891+ Y.lp.app.errors.display_error = old_error_method;
892+
893+ }
894+}));
895+
896 var handle_complete = function(data) {
897 window.status = '::::' + JSON.stringify(data);
898 };
899
900=== modified file 'lib/lp/bugs/templates/bugtask-index.pt'
901--- lib/lp/bugs/templates/bugtask-index.pt 2011-06-15 11:04:53 +0000
902+++ lib/lp/bugs/templates/bugtask-index.pt 2011-06-15 11:05:00 +0000
903@@ -59,7 +59,8 @@
904 container_box: '#other-bug-subscribers',
905 bug: LP.cache.bug,
906 subscribers_details_view:
907- '/+bug-portlet-subscribers-details'
908+ '/+bug-portlet-subscribers-details',
909+ subscribe_someone_else_link: '.menu-link-addsubscriber'
910 });
911 });
912 });