Merge lp:~gary/launchpad/muteteamsub-ui into lp:launchpad/db-devel

Proposed by Gary Poster on 2011-04-05
Status: Merged
Approved by: Gary Poster on 2011-04-05
Approved revision: no longer in the source branch.
Merged at revision: 10396
Proposed branch: lp:~gary/launchpad/muteteamsub-ui
Merge into: lp:launchpad/db-devel
Diff against target: 759 lines (+358/-96)
7 files modified
lib/lp/bugs/browser/structuralsubscription.py (+3/-1)
lib/lp/bugs/browser/tests/test_expose.py (+26/-5)
lib/lp/bugs/interfaces/bugsubscriptionfilter.py (+8/-11)
lib/lp/bugs/model/bugsubscriptionfilter.py (+10/-4)
lib/lp/bugs/tests/test_structuralsubscription.py (+5/-0)
lib/lp/registry/javascript/structural-subscription.js (+106/-37)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+200/-38)
To merge this branch: bzr merge lp:~gary/launchpad/muteteamsub-ui
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2011-04-05 Approve on 2011-04-05
Review via email: mp+56419@code.launchpad.net

Commit message

[r=gmb][bug=751173] Add UI for muting team subscriptions

Description of the change

This branch adds a UI for muting team subscriptions, building on the work Graham and I have done previously.

The Python changes are what I needed to get the UI working.

 * I added a "muted" method. It returns the date it was muted for the person, or None if it is not.
 * I stopped "mute" from returning the mute. I couldn't get lazr.restful to deal with it properly, and it was not in the interface and didn't seem necessary.
 * I removed the webservice bobs from the mute interface because they were not being used (it was not imported into the bug package's webservice.py) and it was not necessary for them to be exported.
 * lib/lp/bugs/browser/structuralsubscription.py exposes the new mute info, and lib/lp/bugs/browser/tests/test_expose.py adds pertinent tests.

The JavaScript in lib/lp/registry/javascript/structural-subscription.js has the meat of the work.
 * I changed edit_subscription_handler to use the currently preferred approach to get the main container by id and then the components by class. I don't remember if I needed to do this or if it just seemed like a good idea at the time. :-/
 * I made some linty changes per our lint (78 char line length) and Crockford's JS linter (he advocates no single line funcs, and does stuff about semi-colons).
 * make_delete_handler now takes a node rather than an id, which seemed a bit more elegant to me.
 * make_delete_handler had messed up its unsubscribe node in some refactoring or other. I fixed it (but still no test).
 * The meat of the change is to add mute handling, as you'd expect, which is primarily in make_mute_handler and handle_mute.
 * While I added the mute code, I also changed how we draw subscriptions to use a string for a template, gradually gathered, rather than nodes one by one. As I say in a comment, this is because whitespace was stripped from left and right when I tried to use Y.Node.create with spaces at the beginning or end of a string.

I also added JS tests for the new mute functionality. To do so, I reused some code from earlier tests, which I factored out into helpers (monkeypatch_LP and make_lp_client_stub). We could probably use those more, but this branch was big enough.

To post a comment you must log in.
Graham Binns (gmb) wrote :

This branch looks good. My only request is that you add some explanatory comments to the tests you've added in lib/lp/registry/javascript/tests/test_structural_subscription.js.

review: Approve (code)
Graham Binns (gmb) wrote :

test_muted_team_member_subscription also needs an explanatory comment.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
2--- lib/lp/bugs/browser/structuralsubscription.py 2011-04-01 20:26:38 +0000
3+++ lib/lp/bugs/browser/structuralsubscription.py 2011-04-05 18:33:51 +0000
4@@ -446,7 +446,9 @@
5 subscriber, rootsite='mainsite'),
6 subscriber_title=subscriber.title,
7 subscriber_is_team=is_team,
8- user_is_team_admin=user_is_team_admin,))
9+ user_is_team_admin=user_is_team_admin,
10+ can_mute=filter.isMuteAllowed(user),
11+ is_muted=filter.muted(user) is not None))
12 info = info.values()
13 info.sort(key=lambda item: item['target_url'])
14 IJSONRequestCache(request).objects['subscription_info'] = info
15
16=== modified file 'lib/lp/bugs/browser/tests/test_expose.py'
17--- lib/lp/bugs/browser/tests/test_expose.py 2011-03-31 15:04:53 +0000
18+++ lib/lp/bugs/browser/tests/test_expose.py 2011-04-05 18:33:51 +0000
19@@ -251,8 +251,10 @@
20 self.assertEqual(len(target_info['filters']), 1) # One filter.
21 filter_info = target_info['filters'][0]
22 self.assertEqual(filter_info['filter'], sub.bug_filters[0])
23- self.failUnless(filter_info['subscriber_is_team'])
24- self.failUnless(filter_info['user_is_team_admin'])
25+ self.assertTrue(filter_info['subscriber_is_team'])
26+ self.assertTrue(filter_info['user_is_team_admin'])
27+ self.assertTrue(filter_info['can_mute'])
28+ self.assertFalse(filter_info['is_muted'])
29 self.assertEqual(filter_info['subscriber_title'], team.title)
30 self.assertEqual(
31 filter_info['subscriber_link'],
32@@ -273,8 +275,10 @@
33 expose_user_subscriptions_to_js(user, [sub], request)
34 info = IJSONRequestCache(request).objects['subscription_info']
35 filter_info = info[0]['filters'][0]
36- self.failUnless(filter_info['subscriber_is_team'])
37- self.failIf(filter_info['user_is_team_admin'])
38+ self.assertTrue(filter_info['subscriber_is_team'])
39+ self.assertFalse(filter_info['user_is_team_admin'])
40+ self.assertTrue(filter_info['can_mute'])
41+ self.assertFalse(filter_info['is_muted'])
42 self.assertEqual(filter_info['subscriber_title'], team.title)
43 self.assertEqual(
44 filter_info['subscriber_link'],
45@@ -283,6 +287,21 @@
46 filter_info['subscriber_url'],
47 canonical_url(team, rootsite='mainsite'))
48
49+ def test_muted_team_member_subscription(self):
50+ # Show that a muted team subscription is correctly represented.
51+ user = self.factory.makePerson()
52+ target = self.factory.makeProduct()
53+ request = LaunchpadTestRequest()
54+ team = self.factory.makeTeam(members=[user])
55+ with person_logged_in(team.teamowner):
56+ sub = target.addBugSubscription(team, team.teamowner)
57+ sub.bug_filters.one().mute(user)
58+ expose_user_subscriptions_to_js(user, [sub], request)
59+ info = IJSONRequestCache(request).objects['subscription_info']
60+ filter_info = info[0]['filters'][0]
61+ self.assertTrue(filter_info['can_mute'])
62+ self.assertTrue(filter_info['is_muted'])
63+
64 def test_self_subscription(self):
65 # Make a subscription directly for the user and see what we record.
66 user = self.factory.makePerson()
67@@ -293,8 +312,10 @@
68 expose_user_subscriptions_to_js(user, [sub], request)
69 info = IJSONRequestCache(request).objects['subscription_info']
70 filter_info = info[0]['filters'][0]
71- self.failIf(filter_info['subscriber_is_team'])
72+ self.assertFalse(filter_info['subscriber_is_team'])
73 self.assertEqual(filter_info['subscriber_title'], user.title)
74+ self.assertFalse(filter_info['can_mute'])
75+ self.assertFalse(filter_info['is_muted'])
76 self.assertEqual(
77 filter_info['subscriber_link'],
78 absoluteURL(user, IWebServiceClientRequest(request)))
79
80=== modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py'
81--- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-01 17:32:41 +0000
82+++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-05 18:33:51 +0000
83@@ -9,7 +9,6 @@
84 "IBugSubscriptionFilterMute",
85 ]
86
87-
88 from lazr.restful.declarations import (
89 call_with,
90 export_as_webservice_entry,
91@@ -18,7 +17,6 @@
92 export_write_operation,
93 exported,
94 operation_for_version,
95- operation_parameters,
96 REQUEST_USER,
97 )
98 from lazr.restful.fields import Reference
99@@ -41,7 +39,6 @@
100 from lp.bugs.interfaces.structuralsubscription import (
101 IStructuralSubscription,
102 )
103-from lp.registry.interfaces.person import IPerson
104 from lp.services.fields import (
105 PersonChoice,
106 SearchTag,
107@@ -110,24 +107,25 @@
108 """Methods on `IBugSubscriptionFilter` that can be called by anyone."""
109
110 @call_with(person=REQUEST_USER)
111- @operation_parameters(
112- person=Reference(IPerson, title=_('Person'), required=True))
113 @export_read_operation()
114 @operation_for_version('devel')
115 def isMuteAllowed(person):
116 """Return True if this filter can be muted for `person`."""
117
118 @call_with(person=REQUEST_USER)
119- @operation_parameters(
120- person=Reference(IPerson, title=_('Person'), required=True))
121+ @export_read_operation()
122+ @operation_for_version('devel')
123+ def muted(person):
124+ """Return date muted if this filter was muted for `person`, or None.
125+ """
126+
127+ @call_with(person=REQUEST_USER)
128 @export_write_operation()
129 @operation_for_version('devel')
130 def mute(person):
131 """Add a mute for `person` to this filter."""
132
133 @call_with(person=REQUEST_USER)
134- @operation_parameters(
135- person=Reference(IPerson, title=_('Person'), required=True))
136 @export_write_operation()
137 @operation_for_version('devel')
138 def unmute(person):
139@@ -155,13 +153,12 @@
140 class IBugSubscriptionFilterMute(Interface):
141 """A mute on an IBugSubscriptionFilter."""
142
143- export_as_webservice_entry()
144-
145 person = PersonChoice(
146 title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',
147 readonly=True, description=_("The person subscribed."))
148 filter = Reference(
149 IBugSubscriptionFilter, title=_("Subscription filter"),
150+ required=True, readonly=True,
151 description=_("The subscription filter to be muted."))
152 date_created = Datetime(
153 title=_("The date on which the mute was created."), required=False,
154
155=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
156--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-04 12:30:20 +0000
157+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-05 18:33:51 +0000
158@@ -259,6 +259,15 @@
159 self.structural_subscription.subscriber.isTeam() and
160 person.inTeam(self.structural_subscription.subscriber))
161
162+ def muted(self, person):
163+ store = Store.of(self)
164+ existing_mutes = store.find(
165+ BugSubscriptionFilterMute,
166+ BugSubscriptionFilterMute.filter_id == self.id,
167+ BugSubscriptionFilterMute.person_id == person.id)
168+ if not existing_mutes.is_empty():
169+ return existing_mutes.one().date_created
170+
171 def mute(self, person):
172 """See `IBugSubscriptionFilter`."""
173 if not self.isMuteAllowed(person):
174@@ -270,14 +279,11 @@
175 BugSubscriptionFilterMute,
176 BugSubscriptionFilterMute.filter_id == self.id,
177 BugSubscriptionFilterMute.person_id == person.id)
178- if not existing_mutes.is_empty():
179- return existing_mutes.one()
180- else:
181+ if existing_mutes.is_empty():
182 mute = BugSubscriptionFilterMute()
183 mute.person = person
184 mute.filter = self.id
185 store.add(mute)
186- return mute
187
188 def unmute(self, person):
189 """See `IBugSubscriptionFilter`."""
190
191=== modified file 'lib/lp/bugs/tests/test_structuralsubscription.py'
192--- lib/lp/bugs/tests/test_structuralsubscription.py 2011-04-01 17:24:42 +0000
193+++ lib/lp/bugs/tests/test_structuralsubscription.py 2011-04-05 18:33:51 +0000
194@@ -733,8 +733,11 @@
195 BugSubscriptionFilterMute.filter == filter_id,
196 BugSubscriptionFilterMute.person == person_id)
197 self.assertTrue(mutes.is_empty())
198+ self.assertFalse(self.filter.muted(self.team_member))
199 self.filter.mute(self.team_member)
200+ self.assertTrue(self.filter.muted(self.team_member))
201 store.flush()
202+ self.assertFalse(mutes.is_empty())
203
204 def test_unmute_removes_mute(self):
205 # BugSubscriptionFilter.unmute() removes any mute for a given
206@@ -749,7 +752,9 @@
207 BugSubscriptionFilterMute.filter == filter_id,
208 BugSubscriptionFilterMute.person == person_id)
209 self.assertFalse(mutes.is_empty())
210+ self.assertTrue(self.filter.muted(self.team_member))
211 self.filter.unmute(self.team_member)
212+ self.assertFalse(self.filter.muted(self.team_member))
213 store.flush()
214 self.assertTrue(mutes.is_empty())
215
216
217=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
218--- lib/lp/registry/javascript/structural-subscription.js 2011-04-04 16:22:20 +0000
219+++ lib/lp/registry/javascript/structural-subscription.js 2011-04-05 18:33:51 +0000
220@@ -249,17 +249,17 @@
221 */
222 function edit_subscription_handler(context, form_data) {
223 var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
224- var filter_id = '#filter-description-'+context.filter_id.toString();
225+ var filter_node = Y.one(
226+ '#subscription-filter-'+context.filter_id.toString());
227 if (has_errors) {
228 return false;
229 }
230 var on = {success: function (new_data) {
231 var filter = new_data.getAttrs();
232- var description_node = Y.one(filter_id);
233- description_node
234+ filter_node.one('.filter-description')
235 .empty()
236 .appendChild(create_filter_description(filter));
237- description_node.ancestor('.subscription-filter').one('.filter-name')
238+ filter_node.one('.filter-name')
239 .empty()
240 .appendChild(render_filter_title(context.filter_info, filter));
241 add_subscription_overlay.hide();
242@@ -652,7 +652,8 @@
243 ' description help</span></a> ' +
244 ' </dd>' +
245 ' <dt>Receive mail for bugs affecting' +
246- ' <span id="structural-subscription-context-title"></span> that</dt>' +
247+ ' <span id="structural-subscription-context-title"></span> '+
248+ ' that</dt>' +
249 ' <dd>' +
250 ' <div id="events">' +
251 ' <input type="radio" name="events"' +
252@@ -690,7 +691,8 @@
253 ' <dt></dt>' +
254 ' <dd style="margin-left:25px;">' +
255 ' <div id="accordion-overlay"' +
256- ' style="position:relative; overflow:hidden;"></div>' +
257+ ' style="position:relative; '+
258+ 'overflow:hidden;"></div>' +
259 ' </dd>' +
260 ' </dl>' +
261 ' </div> ' +
262@@ -1030,10 +1032,10 @@
263 /**
264 * Construct a handler for an unsubscribe link.
265 */
266-function make_delete_handler(filter, filter_id, subscriber_id) {
267+function make_delete_handler(filter, node, subscriber_id) {
268 var error_handler = new Y.lp.client.ErrorHandler();
269 error_handler.showError = function(error_msg) {
270- var unsubscribe_node = Y.one('#unsubscribe-'+filter_id.toString());
271+ var unsubscribe_node = node.one('a.delete-subscription');
272 Y.lp.app.errors.display_error(unsubscribe_node, error_msg);
273 };
274 return function() {
275@@ -1046,9 +1048,8 @@
276 var to_collapse = subscriber;
277 var filters = subscriber.all('.subscription-filter');
278 if (!filters.isEmpty()) {
279- to_collapse = Y.one(
280- '#subscription-filter-'+filter_id.toString());
281- }
282+ to_collapse = node;
283+ }
284 collapse_node(to_collapse);
285 },
286 failure: error_handler.getFailureHandler()
287@@ -1059,6 +1060,39 @@
288 }
289
290 /**
291+ * Construct a handler for a mute link.
292+ */
293+function make_mute_handler(filter_info, node){
294+ var error_handler = new Y.lp.client.ErrorHandler();
295+ error_handler.showError = function(error_msg) {
296+ var mute_node = node.one('a.mute-subscription');
297+ Y.lp.app.errors.display_error(mute_node, error_msg);
298+ };
299+ return function() {
300+ var fname;
301+ if (filter_info.is_muted) {
302+ fname = 'unmute';
303+ } else {
304+ fname = 'mute';
305+ }
306+ var config = {
307+ on: {success: function(){
308+ if (fname === 'mute') {
309+ filter_info.is_muted = true;
310+ } else {
311+ filter_info.is_muted = false;
312+ }
313+ handle_mute(node, filter_info.is_muted);
314+ },
315+ failure: error_handler.getFailureHandler()
316+ }
317+ };
318+ namespace.lp_client.named_post(filter_info.filter.self_link,
319+ fname, config);
320+ };
321+}
322+
323+/**
324 * Attach activation (click) handlers to all of the edit links on the page.
325 */
326 function wire_up_edit_links(config) {
327@@ -1071,17 +1105,20 @@
328 var sub = subscription_info[i];
329 for (j=0; j<sub.filters.length; j++) {
330 var filter_info = sub.filters[j];
331+ var node = Y.one('#subscription-filter-'+filter_id.toString());
332+ if (filter_info.can_mute) {
333+ var mute_link = node.one('a.mute-subscription');
334+ mute_link.on('click', make_mute_handler(filter_info, node));
335+ }
336 if (!filter_info.subscriber_is_team ||
337 filter_info.user_is_team_admin) {
338- var node = Y.one(
339- '#subscription-filter-'+filter_id.toString());
340 var edit_link = node.one('a.edit-subscription');
341 var edit_handler = make_edit_handler(
342 sub, filter_info, filter_id, config);
343 edit_link.on('click', edit_handler);
344 var delete_link = node.one('a.delete-subscription');
345 var delete_handler = make_delete_handler(
346- filter_info.filter, filter_id, i);
347+ filter_info.filter, node, i);
348 delete_link.on('click', delete_handler);
349 }
350 filter_id += 1;
351@@ -1090,6 +1127,26 @@
352 }
353
354 /**
355+ * For a given filter node, set it up properly based on mute state.
356+ */
357+function handle_mute(node, muted) {
358+ var control = node.one('a.mute-subscription');
359+ var label = node.one('em.mute-label');
360+ var description = node.one('.filter-description');
361+ if (muted) {
362+ control.set('text', 'Receive emails from this subscription');
363+ control.replaceClass('no', 'yes');
364+ label.setStyle('display', null);
365+ description.setStyle('color', '#bbb');
366+ } else {
367+ control.set('text', 'Do not receive emails from this subscription');
368+ control.replaceClass('yes', 'no');
369+ label.setStyle('display', 'none');
370+ description.setStyle('color', null);
371+ }
372+}
373+
374+/**
375 * Populate the subscription list DOM element with subscription descriptions.
376 */
377 function fill_in_bug_subscriptions(config) {
378@@ -1133,31 +1190,43 @@
379 filter_node.appendChild(Y.Node.create(
380 '<strong class="filter-name"></strong>'))
381 .appendChild(render_filter_title(sub.filters[j], filter));
382-
383- if (!sub.filters[j].subscriber_is_team ||
384- sub.filters[j].user_is_team_admin) {
385+ if (sub.filters[j].can_mute) {
386+ filter_node.appendChild(Y.Node.create(
387+ '<em class="mute-label" style="padding-left: 1em;">You '+
388+ 'do not receive emails from this subscription.</em>'));
389+ }
390+ var can_edit = (!sub.filters[j].subscriber_is_team ||
391+ sub.filters[j].user_is_team_admin);
392+ // Whitespace is stripped from the left and right of the string
393+ // when you make a node, so we have to build the string with the
394+ // intermediate whitespace and then create the node at the end.
395+ var control_template = '';
396+ if (sub.filters[j].can_mute) {
397+ control_template += (
398+ '<a href="#" class="sprite js-action '+
399+ 'mute-subscription"></a>');
400+ if (can_edit) {
401+ control_template += ' or ';
402+ }
403+ }
404+ if (can_edit) {
405 // User can edit the subscription.
406- filter_node.appendChild(Y.Node.create(
407- '<span style="float: right">'+
408+ control_template += (
409 '<a href="#" class="sprite modify edit js-action '+
410- ' edit-subscription">'+
411- ' Edit this subscription</a> or '+
412+ ' edit-subscription">Edit this subscription</a> or '+
413 '<a href="#" class="sprite modify remove js-action '+
414- ' delete-subscription">'+
415- ' Unsubscribe</a></span>'));
416- } else {
417- // User cannot edit the subscription, because this is a
418- // team and the user does not have admin privileges.
419- filter_node.appendChild(Y.Node.create(
420- '<span style="float: right"><em>'+
421- 'You do not have privileges to change this subscription'+
422- '</em></span>'));
423+ ' delete-subscription">Unsubscribe</a>');
424 }
425-
426- filter_node.appendChild(Y.Node.create(
427- '<div style="padding-left: 1em"></div>')
428- .set('id', 'filter-description-'+filter_id.toString()))
429+ filter_node.appendChild(Y.Node.create(
430+ '<span style="float: right"></span>')
431+ ).appendChild(Y.Node.create(control_template));
432+ filter_node.appendChild(Y.Node.create(
433+ '<div style="padding-left: 1em" '+
434+ 'class="filter-description"></div>'))
435 .appendChild(create_filter_description(filter));
436+ if (sub.filters[j].can_mute) {
437+ handle_mute(filter_node, sub.filters[j].is_muted);
438+ }
439
440 filter_id += 1;
441 }
442@@ -1246,13 +1315,13 @@
443 // Format event details.
444 var events; // When will email be sent?
445 if (filter.bug_notification_level === 'Discussion') {
446- events = 'You will recieve an email when any change '+
447+ events = 'You will receive an email when any change '+
448 'is made or a comment is added.';
449 } else if (filter.bug_notification_level === 'Details') {
450- events = 'You will recieve an email when any changes '+
451+ events = 'You will receive an email when any changes '+
452 'are made to the bug. Bug comments will not be sent.';
453 } else if (filter.bug_notification_level === 'Lifecycle') {
454- events = 'You will recieve an email when bugs are '+
455+ events = 'You will receive an email when bugs are '+
456 'opened or closed.';
457 } else {
458 throw new Error('Unrecognized events.');
459
460=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
461--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-04 15:59:28 +0000
462+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-05 18:33:51 +0000
463@@ -22,6 +22,10 @@
464 var content_box_name = 'ss-content-box';
465 var content_box_id = '#' + content_box_name;
466
467+ // Listing node.
468+ var subscription_listing_name = 'subscription-listing';
469+ var subscription_listing_id = '#' + subscription_listing_name;
470+
471 var target_link_class = '.menu-link-subscribe_to_bug_mail';
472
473 function array_compare(a,b) {
474@@ -36,10 +40,13 @@
475 return true;
476 }
477
478- function create_test_node() {
479+ function create_test_node(include_listing) {
480 return Y.Node.create(
481 '<div id="test-content">' +
482 ' <div id="' + content_box_name + '"></div>' +
483+ (include_listing
484+ ? (' <div id="' + subscription_listing_name + '"></div>')
485+ : '') +
486 '</div>');
487 }
488
489@@ -58,6 +65,61 @@
490 return true;
491 }
492
493+ function monkeypatch_LP() {
494+ // Monkeypatch LP to avoid network traffic and to allow
495+ // insertion of test data.
496+ var original_lp = window.LP
497+ window.LP = {
498+ links: {},
499+ cache: {}
500+ };
501+
502+ LP.cache.context = {
503+ title: 'Test Project',
504+ self_link: 'https://launchpad.dev/api/test_project'
505+ };
506+ LP.cache.administratedTeams = [];
507+ LP.cache.importances = ['Unknown', 'Critical', 'High', 'Medium',
508+ 'Low', 'Wishlist', 'Undecided'];
509+ LP.cache.statuses = ['New', 'Incomplete', 'Opinion',
510+ 'Invalid', 'Won\'t Fix', 'Expired',
511+ 'Confirmed', 'Triaged', 'In Progress',
512+ 'Fix Committed', 'Fix Released', 'Unknown'];
513+ LP.links.me = 'https://launchpad.dev/api/~someone';
514+ return original_lp;
515+ }
516+
517+ function LPClient(){
518+ if (!(this instanceof arguments.callee))
519+ throw new Error("Constructor called as a function");
520+ this.received = []
521+ // We create new functions every time because we allow them to be
522+ // configured.
523+ this.named_post = function(url, func, config) {
524+ this._call('named_post', config, arguments);
525+ };
526+ this.patch = function(bug_filter, data, config) {
527+ this._call('patch', config, arguments);
528+ }
529+ };
530+ LPClient.prototype._call = function(name, config, args) {
531+ this.received.push(
532+ [name, Array.prototype.slice.call(args)]);
533+ if (!Y.Lang.isValue(args.callee.args))
534+ throw new Error("Set call_args on "+name);
535+ if (Y.Lang.isValue(args.callee.fail) && args.callee.fail) {
536+ config.on.failure.apply(undefined, args.callee.args);
537+ } else {
538+ config.on.success.apply(undefined, args.callee.args);
539+ }
540+ };
541+ // DELETE uses Y.io directly as of this writing, so we cannot stub it
542+ // here.
543+
544+ function make_lp_client_stub() {
545+ return new LPClient();
546+ }
547+
548 test_case = new Y.Test.Case({
549 name: 'structural_subscription_overlay',
550
551@@ -467,28 +529,11 @@
552 setUp: function() {
553 // Monkeypatch LP to avoid network traffic and to allow
554 // insertion of test data.
555- window.LP = {
556- links: {},
557- cache: {}
558- };
559-
560- LP.cache.context = {
561- title: 'Test Project',
562- self_link: 'https://launchpad.dev/api/test_project'
563- };
564- LP.cache.administratedTeams = [];
565- LP.cache.importances = ['Unknown', 'Critical', 'High', 'Medium',
566- 'Low', 'Wishlist', 'Undecided'];
567- LP.cache.statuses = ['New', 'Incomplete', 'Opinion',
568- 'Invalid', 'Won\'t Fix', 'Expired',
569- 'Confirmed', 'Triaged', 'In Progress',
570- 'Fix Committed', 'Fix Released', 'Unknown'];
571- LP.links.me = 'https://launchpad.dev/api/~someone';
572-
573- var lp_client = function() {};
574+ this.original_lp = monkeypatch_LP();
575+
576 this.configuration = {
577 content_box: content_box_id,
578- lp_client: lp_client
579+ lp_client: make_lp_client_stub()
580 };
581
582 this.content_node = create_test_node();
583@@ -496,17 +541,16 @@
584 },
585
586 tearDown: function() {
587- remove_test_node();
588- delete this.content_node;
589+ window.LP = this.original_lp;
590+ remove_test_node();
591+ delete this.content_node;
592 },
593
594 test_overlay_error_handling_adding: function() {
595 // Verify that errors generated during adding of a filter are
596 // displayed to the user.
597- this.configuration.lp_client.named_post =
598- function(url, func, config) {
599- config.on.failure(true, true);
600- };
601+ this.configuration.lp_client.named_post.fail = true;
602+ this.configuration.lp_client.named_post.args = [true, true];
603 module.setup(this.configuration);
604 module._show_add_overlay(this.configuration);
605 // After the setup the overlay should be in the DOM.
606@@ -526,17 +570,10 @@
607 // displayed to the user.
608 var original_delete_filter = module._delete_filter;
609 module._delete_filter = function() {};
610- this.configuration.lp_client.patch =
611- function(bug_filter, data, config) {
612- config.on.failure(true, true);
613- };
614- var bug_filter = {
615- 'getAttrs': function() { return {}; }
616- };
617- this.configuration.lp_client.named_post =
618- function(url, func, config) {
619- config.on.success(bug_filter);
620- };
621+ this.configuration.lp_client.patch.fail = true;
622+ this.configuration.lp_client.patch.args = [true, true];
623+ this.configuration.lp_client.named_post.args = [
624+ {'getAttrs': function() { return {}; }}];
625 module.setup(this.configuration);
626 module._show_add_overlay(this.configuration);
627 // After the setup the overlay should be in the DOM.
628@@ -794,6 +831,131 @@
629
630 }));
631
632+ suite.add(new Y.Test.Case({
633+ name: 'Structural Subscription mute team subscriptions',
634+
635+ // Verify that the mute controls and labels on the edit block
636+ // render and interact properly
637+
638+ _should: {
639+ error: {
640+ }
641+ },
642+
643+ setUp: function() {
644+ // Monkeypatch LP to avoid network traffic and to allow
645+ // insertion of test data.
646+ this.original_lp = monkeypatch_LP();
647+ this.test_node = create_test_node(true);
648+ Y.one('body').appendChild(this.test_node);
649+ this.lp_client = make_lp_client_stub();
650+ LP.cache.subscription_info = [
651+ {target_url: 'http://example.com',
652+ target_title:'Example project',
653+ filters: [
654+ {filter: {
655+ statuses: [],
656+ importances: [],
657+ tags: [],
658+ find_all_tags: true,
659+ bug_notification_level: 'Discussion',
660+ self_link: 'http://example.com/a_filter'
661+ },
662+ can_mute: true,
663+ is_muted: false,
664+ subscriber_is_team: true,
665+ subscriber_url: 'http://example.com/subscriber',
666+ subscriber_title: 'Thidwick',
667+ user_is_team_admin: false,
668+ }
669+ ]
670+ }
671+ ]
672+ },
673+
674+ tearDown: function() {
675+ remove_test_node();
676+ window.LP = this.original_lp;
677+ },
678+
679+ test_not_muted_rendering: function() {
680+ // Verify that an unmuted subscription is rendered correctly.
681+ module.setup_bug_subscriptions(
682+ {content_box: content_box_id,
683+ lp_client: this.lp_client});
684+ var listing = this.test_node.one(subscription_listing_id);
685+ var filter_node = listing.one('#subscription-filter-0');
686+ Assert.isNotNull(filter_node);
687+ var mute_label_node = filter_node.one('.mute-label');
688+ Assert.isNotNull(mute_label_node);
689+ Assert.areEqual(mute_label_node.getStyle('display'), 'none');
690+ var mute_link = filter_node.one('a.mute-subscription');
691+ Assert.isNotNull(mute_link);
692+ Assert.isTrue(mute_link.hasClass('no'));
693+ },
694+
695+ test_muted_rendering: function() {
696+ // Verify that a muted subscription is rendered correctly.
697+ LP.cache.subscription_info[0].filters[0].is_muted = true;
698+ module.setup_bug_subscriptions(
699+ {content_box: content_box_id,
700+ lp_client: this.lp_client});
701+ var listing = this.test_node.one(subscription_listing_id);
702+ var filter_node = listing.one('#subscription-filter-0');
703+ Assert.isNotNull(filter_node);
704+ var mute_label_node = filter_node.one('.mute-label');
705+ Assert.isNotNull(mute_label_node);
706+ Assert.areEqual(mute_label_node.getStyle('display'), 'inline');
707+ var mute_link = filter_node.one('a.mute-subscription');
708+ Assert.isNotNull(mute_link);
709+ Assert.isTrue(mute_link.hasClass('yes'));
710+ },
711+
712+ test_not_muted_toggle_muted: function() {
713+ // Verify that an unmuted subscription can be muted.
714+ module.setup_bug_subscriptions(
715+ {content_box: content_box_id,
716+ lp_client: this.lp_client});
717+ var listing = this.test_node.one(subscription_listing_id);
718+ var filter_node = listing.one('#subscription-filter-0');
719+ var mute_label_node = filter_node.one('.mute-label');
720+ var mute_link = filter_node.one('a.mute-subscription');
721+ this.lp_client.named_post.args = []
722+ Y.Event.simulate(Y.Node.getDOMNode(mute_link), 'click');
723+ Assert.areEqual(this.lp_client.received[0][0], 'named_post');
724+ Assert.areEqual(
725+ this.lp_client.received[0][1][0],
726+ 'http://example.com/a_filter');
727+ Assert.areEqual(
728+ this.lp_client.received[0][1][1], 'mute');
729+ Assert.areEqual(mute_label_node.getStyle('display'), 'inline');
730+ Assert.isTrue(mute_link.hasClass('yes'));
731+ },
732+
733+ test_muted_toggle_not_muted: function() {
734+ // Verify that an muted subscription can be unmuted.
735+ LP.cache.subscription_info[0].filters[0].is_muted = true;
736+ module.setup_bug_subscriptions(
737+ {content_box: content_box_id,
738+ lp_client: this.lp_client});
739+ var listing = this.test_node.one(subscription_listing_id);
740+ var filter_node = listing.one('#subscription-filter-0');
741+ var mute_label_node = filter_node.one('.mute-label');
742+ var mute_link = filter_node.one('a.mute-subscription');
743+ this.lp_client.named_post.args = []
744+ Y.Event.simulate(Y.Node.getDOMNode(mute_link), 'click');
745+ Assert.areEqual(this.lp_client.received[0][0], 'named_post');
746+ Assert.areEqual(
747+ this.lp_client.received[0][1][0],
748+ 'http://example.com/a_filter');
749+ Assert.areEqual(
750+ this.lp_client.received[0][1][1], 'unmute');
751+ Assert.areEqual(mute_label_node.getStyle('display'), 'none');
752+ Assert.isTrue(mute_link.hasClass('no'));
753+ }
754+
755+ }));
756+
757 // Lock, stock, and two smoking barrels.
758 var handle_complete = function(data) {
759 var status_node = Y.Node.create(

Subscribers

People subscribed via source and target branches

to status/vote changes: