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

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
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 Approve
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.
Revision history for this message
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)
Revision history for this message
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
=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py 2011-04-01 20:26:38 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py 2011-04-05 18:33:51 +0000
@@ -446,7 +446,9 @@
446 subscriber, rootsite='mainsite'),446 subscriber, rootsite='mainsite'),
447 subscriber_title=subscriber.title,447 subscriber_title=subscriber.title,
448 subscriber_is_team=is_team,448 subscriber_is_team=is_team,
449 user_is_team_admin=user_is_team_admin,))449 user_is_team_admin=user_is_team_admin,
450 can_mute=filter.isMuteAllowed(user),
451 is_muted=filter.muted(user) is not None))
450 info = info.values()452 info = info.values()
451 info.sort(key=lambda item: item['target_url'])453 info.sort(key=lambda item: item['target_url'])
452 IJSONRequestCache(request).objects['subscription_info'] = info454 IJSONRequestCache(request).objects['subscription_info'] = info
453455
=== modified file 'lib/lp/bugs/browser/tests/test_expose.py'
--- lib/lp/bugs/browser/tests/test_expose.py 2011-03-31 15:04:53 +0000
+++ lib/lp/bugs/browser/tests/test_expose.py 2011-04-05 18:33:51 +0000
@@ -251,8 +251,10 @@
251 self.assertEqual(len(target_info['filters']), 1) # One filter.251 self.assertEqual(len(target_info['filters']), 1) # One filter.
252 filter_info = target_info['filters'][0]252 filter_info = target_info['filters'][0]
253 self.assertEqual(filter_info['filter'], sub.bug_filters[0])253 self.assertEqual(filter_info['filter'], sub.bug_filters[0])
254 self.failUnless(filter_info['subscriber_is_team'])254 self.assertTrue(filter_info['subscriber_is_team'])
255 self.failUnless(filter_info['user_is_team_admin'])255 self.assertTrue(filter_info['user_is_team_admin'])
256 self.assertTrue(filter_info['can_mute'])
257 self.assertFalse(filter_info['is_muted'])
256 self.assertEqual(filter_info['subscriber_title'], team.title)258 self.assertEqual(filter_info['subscriber_title'], team.title)
257 self.assertEqual(259 self.assertEqual(
258 filter_info['subscriber_link'],260 filter_info['subscriber_link'],
@@ -273,8 +275,10 @@
273 expose_user_subscriptions_to_js(user, [sub], request)275 expose_user_subscriptions_to_js(user, [sub], request)
274 info = IJSONRequestCache(request).objects['subscription_info']276 info = IJSONRequestCache(request).objects['subscription_info']
275 filter_info = info[0]['filters'][0]277 filter_info = info[0]['filters'][0]
276 self.failUnless(filter_info['subscriber_is_team'])278 self.assertTrue(filter_info['subscriber_is_team'])
277 self.failIf(filter_info['user_is_team_admin'])279 self.assertFalse(filter_info['user_is_team_admin'])
280 self.assertTrue(filter_info['can_mute'])
281 self.assertFalse(filter_info['is_muted'])
278 self.assertEqual(filter_info['subscriber_title'], team.title)282 self.assertEqual(filter_info['subscriber_title'], team.title)
279 self.assertEqual(283 self.assertEqual(
280 filter_info['subscriber_link'],284 filter_info['subscriber_link'],
@@ -283,6 +287,21 @@
283 filter_info['subscriber_url'],287 filter_info['subscriber_url'],
284 canonical_url(team, rootsite='mainsite'))288 canonical_url(team, rootsite='mainsite'))
285289
290 def test_muted_team_member_subscription(self):
291 # Show that a muted team subscription is correctly represented.
292 user = self.factory.makePerson()
293 target = self.factory.makeProduct()
294 request = LaunchpadTestRequest()
295 team = self.factory.makeTeam(members=[user])
296 with person_logged_in(team.teamowner):
297 sub = target.addBugSubscription(team, team.teamowner)
298 sub.bug_filters.one().mute(user)
299 expose_user_subscriptions_to_js(user, [sub], request)
300 info = IJSONRequestCache(request).objects['subscription_info']
301 filter_info = info[0]['filters'][0]
302 self.assertTrue(filter_info['can_mute'])
303 self.assertTrue(filter_info['is_muted'])
304
286 def test_self_subscription(self):305 def test_self_subscription(self):
287 # Make a subscription directly for the user and see what we record.306 # Make a subscription directly for the user and see what we record.
288 user = self.factory.makePerson()307 user = self.factory.makePerson()
@@ -293,8 +312,10 @@
293 expose_user_subscriptions_to_js(user, [sub], request)312 expose_user_subscriptions_to_js(user, [sub], request)
294 info = IJSONRequestCache(request).objects['subscription_info']313 info = IJSONRequestCache(request).objects['subscription_info']
295 filter_info = info[0]['filters'][0]314 filter_info = info[0]['filters'][0]
296 self.failIf(filter_info['subscriber_is_team'])315 self.assertFalse(filter_info['subscriber_is_team'])
297 self.assertEqual(filter_info['subscriber_title'], user.title)316 self.assertEqual(filter_info['subscriber_title'], user.title)
317 self.assertFalse(filter_info['can_mute'])
318 self.assertFalse(filter_info['is_muted'])
298 self.assertEqual(319 self.assertEqual(
299 filter_info['subscriber_link'],320 filter_info['subscriber_link'],
300 absoluteURL(user, IWebServiceClientRequest(request)))321 absoluteURL(user, IWebServiceClientRequest(request)))
301322
=== modified file 'lib/lp/bugs/interfaces/bugsubscriptionfilter.py'
--- lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-01 17:32:41 +0000
+++ lib/lp/bugs/interfaces/bugsubscriptionfilter.py 2011-04-05 18:33:51 +0000
@@ -9,7 +9,6 @@
9 "IBugSubscriptionFilterMute",9 "IBugSubscriptionFilterMute",
10 ]10 ]
1111
12
13from lazr.restful.declarations import (12from lazr.restful.declarations import (
14 call_with,13 call_with,
15 export_as_webservice_entry,14 export_as_webservice_entry,
@@ -18,7 +17,6 @@
18 export_write_operation,17 export_write_operation,
19 exported,18 exported,
20 operation_for_version,19 operation_for_version,
21 operation_parameters,
22 REQUEST_USER,20 REQUEST_USER,
23 )21 )
24from lazr.restful.fields import Reference22from lazr.restful.fields import Reference
@@ -41,7 +39,6 @@
41from lp.bugs.interfaces.structuralsubscription import (39from lp.bugs.interfaces.structuralsubscription import (
42 IStructuralSubscription,40 IStructuralSubscription,
43 )41 )
44from lp.registry.interfaces.person import IPerson
45from lp.services.fields import (42from lp.services.fields import (
46 PersonChoice,43 PersonChoice,
47 SearchTag,44 SearchTag,
@@ -110,24 +107,25 @@
110 """Methods on `IBugSubscriptionFilter` that can be called by anyone."""107 """Methods on `IBugSubscriptionFilter` that can be called by anyone."""
111108
112 @call_with(person=REQUEST_USER)109 @call_with(person=REQUEST_USER)
113 @operation_parameters(
114 person=Reference(IPerson, title=_('Person'), required=True))
115 @export_read_operation()110 @export_read_operation()
116 @operation_for_version('devel')111 @operation_for_version('devel')
117 def isMuteAllowed(person):112 def isMuteAllowed(person):
118 """Return True if this filter can be muted for `person`."""113 """Return True if this filter can be muted for `person`."""
119114
120 @call_with(person=REQUEST_USER)115 @call_with(person=REQUEST_USER)
121 @operation_parameters(116 @export_read_operation()
122 person=Reference(IPerson, title=_('Person'), required=True))117 @operation_for_version('devel')
118 def muted(person):
119 """Return date muted if this filter was muted for `person`, or None.
120 """
121
122 @call_with(person=REQUEST_USER)
123 @export_write_operation()123 @export_write_operation()
124 @operation_for_version('devel')124 @operation_for_version('devel')
125 def mute(person):125 def mute(person):
126 """Add a mute for `person` to this filter."""126 """Add a mute for `person` to this filter."""
127127
128 @call_with(person=REQUEST_USER)128 @call_with(person=REQUEST_USER)
129 @operation_parameters(
130 person=Reference(IPerson, title=_('Person'), required=True))
131 @export_write_operation()129 @export_write_operation()
132 @operation_for_version('devel')130 @operation_for_version('devel')
133 def unmute(person):131 def unmute(person):
@@ -155,13 +153,12 @@
155class IBugSubscriptionFilterMute(Interface):153class IBugSubscriptionFilterMute(Interface):
156 """A mute on an IBugSubscriptionFilter."""154 """A mute on an IBugSubscriptionFilter."""
157155
158 export_as_webservice_entry()
159
160 person = PersonChoice(156 person = PersonChoice(
161 title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',157 title=_('Person'), required=True, vocabulary='ValidPersonOrTeam',
162 readonly=True, description=_("The person subscribed."))158 readonly=True, description=_("The person subscribed."))
163 filter = Reference(159 filter = Reference(
164 IBugSubscriptionFilter, title=_("Subscription filter"),160 IBugSubscriptionFilter, title=_("Subscription filter"),
161 required=True, readonly=True,
165 description=_("The subscription filter to be muted."))162 description=_("The subscription filter to be muted."))
166 date_created = Datetime(163 date_created = Datetime(
167 title=_("The date on which the mute was created."), required=False,164 title=_("The date on which the mute was created."), required=False,
168165
=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-04 12:30:20 +0000
+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-05 18:33:51 +0000
@@ -259,6 +259,15 @@
259 self.structural_subscription.subscriber.isTeam() and259 self.structural_subscription.subscriber.isTeam() and
260 person.inTeam(self.structural_subscription.subscriber))260 person.inTeam(self.structural_subscription.subscriber))
261261
262 def muted(self, person):
263 store = Store.of(self)
264 existing_mutes = store.find(
265 BugSubscriptionFilterMute,
266 BugSubscriptionFilterMute.filter_id == self.id,
267 BugSubscriptionFilterMute.person_id == person.id)
268 if not existing_mutes.is_empty():
269 return existing_mutes.one().date_created
270
262 def mute(self, person):271 def mute(self, person):
263 """See `IBugSubscriptionFilter`."""272 """See `IBugSubscriptionFilter`."""
264 if not self.isMuteAllowed(person):273 if not self.isMuteAllowed(person):
@@ -270,14 +279,11 @@
270 BugSubscriptionFilterMute,279 BugSubscriptionFilterMute,
271 BugSubscriptionFilterMute.filter_id == self.id,280 BugSubscriptionFilterMute.filter_id == self.id,
272 BugSubscriptionFilterMute.person_id == person.id)281 BugSubscriptionFilterMute.person_id == person.id)
273 if not existing_mutes.is_empty():282 if existing_mutes.is_empty():
274 return existing_mutes.one()
275 else:
276 mute = BugSubscriptionFilterMute()283 mute = BugSubscriptionFilterMute()
277 mute.person = person284 mute.person = person
278 mute.filter = self.id285 mute.filter = self.id
279 store.add(mute)286 store.add(mute)
280 return mute
281287
282 def unmute(self, person):288 def unmute(self, person):
283 """See `IBugSubscriptionFilter`."""289 """See `IBugSubscriptionFilter`."""
284290
=== modified file 'lib/lp/bugs/tests/test_structuralsubscription.py'
--- lib/lp/bugs/tests/test_structuralsubscription.py 2011-04-01 17:24:42 +0000
+++ lib/lp/bugs/tests/test_structuralsubscription.py 2011-04-05 18:33:51 +0000
@@ -733,8 +733,11 @@
733 BugSubscriptionFilterMute.filter == filter_id,733 BugSubscriptionFilterMute.filter == filter_id,
734 BugSubscriptionFilterMute.person == person_id)734 BugSubscriptionFilterMute.person == person_id)
735 self.assertTrue(mutes.is_empty())735 self.assertTrue(mutes.is_empty())
736 self.assertFalse(self.filter.muted(self.team_member))
736 self.filter.mute(self.team_member)737 self.filter.mute(self.team_member)
738 self.assertTrue(self.filter.muted(self.team_member))
737 store.flush()739 store.flush()
740 self.assertFalse(mutes.is_empty())
738741
739 def test_unmute_removes_mute(self):742 def test_unmute_removes_mute(self):
740 # BugSubscriptionFilter.unmute() removes any mute for a given743 # BugSubscriptionFilter.unmute() removes any mute for a given
@@ -749,7 +752,9 @@
749 BugSubscriptionFilterMute.filter == filter_id,752 BugSubscriptionFilterMute.filter == filter_id,
750 BugSubscriptionFilterMute.person == person_id)753 BugSubscriptionFilterMute.person == person_id)
751 self.assertFalse(mutes.is_empty())754 self.assertFalse(mutes.is_empty())
755 self.assertTrue(self.filter.muted(self.team_member))
752 self.filter.unmute(self.team_member)756 self.filter.unmute(self.team_member)
757 self.assertFalse(self.filter.muted(self.team_member))
753 store.flush()758 store.flush()
754 self.assertTrue(mutes.is_empty())759 self.assertTrue(mutes.is_empty())
755760
756761
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js 2011-04-04 16:22:20 +0000
+++ lib/lp/registry/javascript/structural-subscription.js 2011-04-05 18:33:51 +0000
@@ -249,17 +249,17 @@
249 */249 */
250function edit_subscription_handler(context, form_data) {250function edit_subscription_handler(context, form_data) {
251 var has_errors = check_for_errors_in_overlay(add_subscription_overlay);251 var has_errors = check_for_errors_in_overlay(add_subscription_overlay);
252 var filter_id = '#filter-description-'+context.filter_id.toString();252 var filter_node = Y.one(
253 '#subscription-filter-'+context.filter_id.toString());
253 if (has_errors) {254 if (has_errors) {
254 return false;255 return false;
255 }256 }
256 var on = {success: function (new_data) {257 var on = {success: function (new_data) {
257 var filter = new_data.getAttrs();258 var filter = new_data.getAttrs();
258 var description_node = Y.one(filter_id);259 filter_node.one('.filter-description')
259 description_node
260 .empty()260 .empty()
261 .appendChild(create_filter_description(filter));261 .appendChild(create_filter_description(filter));
262 description_node.ancestor('.subscription-filter').one('.filter-name')262 filter_node.one('.filter-name')
263 .empty()263 .empty()
264 .appendChild(render_filter_title(context.filter_info, filter));264 .appendChild(render_filter_title(context.filter_info, filter));
265 add_subscription_overlay.hide();265 add_subscription_overlay.hide();
@@ -652,7 +652,8 @@
652 ' description help</span></a> ' +652 ' description help</span></a> ' +
653 ' </dd>' +653 ' </dd>' +
654 ' <dt>Receive mail for bugs affecting' +654 ' <dt>Receive mail for bugs affecting' +
655 ' <span id="structural-subscription-context-title"></span> that</dt>' +655 ' <span id="structural-subscription-context-title"></span> '+
656 ' that</dt>' +
656 ' <dd>' +657 ' <dd>' +
657 ' <div id="events">' +658 ' <div id="events">' +
658 ' <input type="radio" name="events"' +659 ' <input type="radio" name="events"' +
@@ -690,7 +691,8 @@
690 ' <dt></dt>' +691 ' <dt></dt>' +
691 ' <dd style="margin-left:25px;">' +692 ' <dd style="margin-left:25px;">' +
692 ' <div id="accordion-overlay"' +693 ' <div id="accordion-overlay"' +
693 ' style="position:relative; overflow:hidden;"></div>' +694 ' style="position:relative; '+
695 'overflow:hidden;"></div>' +
694 ' </dd>' +696 ' </dd>' +
695 ' </dl>' +697 ' </dl>' +
696 ' </div> ' +698 ' </div> ' +
@@ -1030,10 +1032,10 @@
1030/**1032/**
1031 * Construct a handler for an unsubscribe link.1033 * Construct a handler for an unsubscribe link.
1032 */1034 */
1033function make_delete_handler(filter, filter_id, subscriber_id) {1035function make_delete_handler(filter, node, subscriber_id) {
1034 var error_handler = new Y.lp.client.ErrorHandler();1036 var error_handler = new Y.lp.client.ErrorHandler();
1035 error_handler.showError = function(error_msg) {1037 error_handler.showError = function(error_msg) {
1036 var unsubscribe_node = Y.one('#unsubscribe-'+filter_id.toString());1038 var unsubscribe_node = node.one('a.delete-subscription');
1037 Y.lp.app.errors.display_error(unsubscribe_node, error_msg);1039 Y.lp.app.errors.display_error(unsubscribe_node, error_msg);
1038 };1040 };
1039 return function() {1041 return function() {
@@ -1046,9 +1048,8 @@
1046 var to_collapse = subscriber;1048 var to_collapse = subscriber;
1047 var filters = subscriber.all('.subscription-filter');1049 var filters = subscriber.all('.subscription-filter');
1048 if (!filters.isEmpty()) {1050 if (!filters.isEmpty()) {
1049 to_collapse = Y.one(1051 to_collapse = node;
1050 '#subscription-filter-'+filter_id.toString());1052 }
1051 }
1052 collapse_node(to_collapse);1053 collapse_node(to_collapse);
1053 },1054 },
1054 failure: error_handler.getFailureHandler()1055 failure: error_handler.getFailureHandler()
@@ -1059,6 +1060,39 @@
1059}1060}
10601061
1061/**1062/**
1063 * Construct a handler for a mute link.
1064 */
1065function make_mute_handler(filter_info, node){
1066 var error_handler = new Y.lp.client.ErrorHandler();
1067 error_handler.showError = function(error_msg) {
1068 var mute_node = node.one('a.mute-subscription');
1069 Y.lp.app.errors.display_error(mute_node, error_msg);
1070 };
1071 return function() {
1072 var fname;
1073 if (filter_info.is_muted) {
1074 fname = 'unmute';
1075 } else {
1076 fname = 'mute';
1077 }
1078 var config = {
1079 on: {success: function(){
1080 if (fname === 'mute') {
1081 filter_info.is_muted = true;
1082 } else {
1083 filter_info.is_muted = false;
1084 }
1085 handle_mute(node, filter_info.is_muted);
1086 },
1087 failure: error_handler.getFailureHandler()
1088 }
1089 };
1090 namespace.lp_client.named_post(filter_info.filter.self_link,
1091 fname, config);
1092 };
1093}
1094
1095/**
1062 * Attach activation (click) handlers to all of the edit links on the page.1096 * Attach activation (click) handlers to all of the edit links on the page.
1063 */1097 */
1064function wire_up_edit_links(config) {1098function wire_up_edit_links(config) {
@@ -1071,17 +1105,20 @@
1071 var sub = subscription_info[i];1105 var sub = subscription_info[i];
1072 for (j=0; j<sub.filters.length; j++) {1106 for (j=0; j<sub.filters.length; j++) {
1073 var filter_info = sub.filters[j];1107 var filter_info = sub.filters[j];
1108 var node = Y.one('#subscription-filter-'+filter_id.toString());
1109 if (filter_info.can_mute) {
1110 var mute_link = node.one('a.mute-subscription');
1111 mute_link.on('click', make_mute_handler(filter_info, node));
1112 }
1074 if (!filter_info.subscriber_is_team ||1113 if (!filter_info.subscriber_is_team ||
1075 filter_info.user_is_team_admin) {1114 filter_info.user_is_team_admin) {
1076 var node = Y.one(
1077 '#subscription-filter-'+filter_id.toString());
1078 var edit_link = node.one('a.edit-subscription');1115 var edit_link = node.one('a.edit-subscription');
1079 var edit_handler = make_edit_handler(1116 var edit_handler = make_edit_handler(
1080 sub, filter_info, filter_id, config);1117 sub, filter_info, filter_id, config);
1081 edit_link.on('click', edit_handler);1118 edit_link.on('click', edit_handler);
1082 var delete_link = node.one('a.delete-subscription');1119 var delete_link = node.one('a.delete-subscription');
1083 var delete_handler = make_delete_handler(1120 var delete_handler = make_delete_handler(
1084 filter_info.filter, filter_id, i);1121 filter_info.filter, node, i);
1085 delete_link.on('click', delete_handler);1122 delete_link.on('click', delete_handler);
1086 }1123 }
1087 filter_id += 1;1124 filter_id += 1;
@@ -1090,6 +1127,26 @@
1090}1127}
10911128
1092/**1129/**
1130 * For a given filter node, set it up properly based on mute state.
1131 */
1132function handle_mute(node, muted) {
1133 var control = node.one('a.mute-subscription');
1134 var label = node.one('em.mute-label');
1135 var description = node.one('.filter-description');
1136 if (muted) {
1137 control.set('text', 'Receive emails from this subscription');
1138 control.replaceClass('no', 'yes');
1139 label.setStyle('display', null);
1140 description.setStyle('color', '#bbb');
1141 } else {
1142 control.set('text', 'Do not receive emails from this subscription');
1143 control.replaceClass('yes', 'no');
1144 label.setStyle('display', 'none');
1145 description.setStyle('color', null);
1146 }
1147}
1148
1149/**
1093 * Populate the subscription list DOM element with subscription descriptions.1150 * Populate the subscription list DOM element with subscription descriptions.
1094 */1151 */
1095function fill_in_bug_subscriptions(config) {1152function fill_in_bug_subscriptions(config) {
@@ -1133,31 +1190,43 @@
1133 filter_node.appendChild(Y.Node.create(1190 filter_node.appendChild(Y.Node.create(
1134 '<strong class="filter-name"></strong>'))1191 '<strong class="filter-name"></strong>'))
1135 .appendChild(render_filter_title(sub.filters[j], filter));1192 .appendChild(render_filter_title(sub.filters[j], filter));
11361193 if (sub.filters[j].can_mute) {
1137 if (!sub.filters[j].subscriber_is_team ||1194 filter_node.appendChild(Y.Node.create(
1138 sub.filters[j].user_is_team_admin) {1195 '<em class="mute-label" style="padding-left: 1em;">You '+
1196 'do not receive emails from this subscription.</em>'));
1197 }
1198 var can_edit = (!sub.filters[j].subscriber_is_team ||
1199 sub.filters[j].user_is_team_admin);
1200 // Whitespace is stripped from the left and right of the string
1201 // when you make a node, so we have to build the string with the
1202 // intermediate whitespace and then create the node at the end.
1203 var control_template = '';
1204 if (sub.filters[j].can_mute) {
1205 control_template += (
1206 '<a href="#" class="sprite js-action '+
1207 'mute-subscription"></a>');
1208 if (can_edit) {
1209 control_template += ' or ';
1210 }
1211 }
1212 if (can_edit) {
1139 // User can edit the subscription.1213 // User can edit the subscription.
1140 filter_node.appendChild(Y.Node.create(1214 control_template += (
1141 '<span style="float: right">'+
1142 '<a href="#" class="sprite modify edit js-action '+1215 '<a href="#" class="sprite modify edit js-action '+
1143 ' edit-subscription">'+1216 ' edit-subscription">Edit this subscription</a> or '+
1144 ' Edit this subscription</a> or '+
1145 '<a href="#" class="sprite modify remove js-action '+1217 '<a href="#" class="sprite modify remove js-action '+
1146 ' delete-subscription">'+1218 ' delete-subscription">Unsubscribe</a>');
1147 ' Unsubscribe</a></span>'));
1148 } else {
1149 // User cannot edit the subscription, because this is a
1150 // team and the user does not have admin privileges.
1151 filter_node.appendChild(Y.Node.create(
1152 '<span style="float: right"><em>'+
1153 'You do not have privileges to change this subscription'+
1154 '</em></span>'));
1155 }1219 }
11561220 filter_node.appendChild(Y.Node.create(
1157 filter_node.appendChild(Y.Node.create(1221 '<span style="float: right"></span>')
1158 '<div style="padding-left: 1em"></div>')1222 ).appendChild(Y.Node.create(control_template));
1159 .set('id', 'filter-description-'+filter_id.toString()))1223 filter_node.appendChild(Y.Node.create(
1224 '<div style="padding-left: 1em" '+
1225 'class="filter-description"></div>'))
1160 .appendChild(create_filter_description(filter));1226 .appendChild(create_filter_description(filter));
1227 if (sub.filters[j].can_mute) {
1228 handle_mute(filter_node, sub.filters[j].is_muted);
1229 }
11611230
1162 filter_id += 1;1231 filter_id += 1;
1163 }1232 }
@@ -1246,13 +1315,13 @@
1246 // Format event details.1315 // Format event details.
1247 var events; // When will email be sent?1316 var events; // When will email be sent?
1248 if (filter.bug_notification_level === 'Discussion') {1317 if (filter.bug_notification_level === 'Discussion') {
1249 events = 'You will recieve an email when any change '+1318 events = 'You will receive an email when any change '+
1250 'is made or a comment is added.';1319 'is made or a comment is added.';
1251 } else if (filter.bug_notification_level === 'Details') {1320 } else if (filter.bug_notification_level === 'Details') {
1252 events = 'You will recieve an email when any changes '+1321 events = 'You will receive an email when any changes '+
1253 'are made to the bug. Bug comments will not be sent.';1322 'are made to the bug. Bug comments will not be sent.';
1254 } else if (filter.bug_notification_level === 'Lifecycle') {1323 } else if (filter.bug_notification_level === 'Lifecycle') {
1255 events = 'You will recieve an email when bugs are '+1324 events = 'You will receive an email when bugs are '+
1256 'opened or closed.';1325 'opened or closed.';
1257 } else {1326 } else {
1258 throw new Error('Unrecognized events.');1327 throw new Error('Unrecognized events.');
12591328
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-04 15:59:28 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-04-05 18:33:51 +0000
@@ -22,6 +22,10 @@
22 var content_box_name = 'ss-content-box';22 var content_box_name = 'ss-content-box';
23 var content_box_id = '#' + content_box_name;23 var content_box_id = '#' + content_box_name;
2424
25 // Listing node.
26 var subscription_listing_name = 'subscription-listing';
27 var subscription_listing_id = '#' + subscription_listing_name;
28
25 var target_link_class = '.menu-link-subscribe_to_bug_mail';29 var target_link_class = '.menu-link-subscribe_to_bug_mail';
2630
27 function array_compare(a,b) {31 function array_compare(a,b) {
@@ -36,10 +40,13 @@
36 return true;40 return true;
37 }41 }
3842
39 function create_test_node() {43 function create_test_node(include_listing) {
40 return Y.Node.create(44 return Y.Node.create(
41 '<div id="test-content">' +45 '<div id="test-content">' +
42 ' <div id="' + content_box_name + '"></div>' +46 ' <div id="' + content_box_name + '"></div>' +
47 (include_listing
48 ? (' <div id="' + subscription_listing_name + '"></div>')
49 : '') +
43 '</div>');50 '</div>');
44 }51 }
4552
@@ -58,6 +65,61 @@
58 return true;65 return true;
59 }66 }
6067
68 function monkeypatch_LP() {
69 // Monkeypatch LP to avoid network traffic and to allow
70 // insertion of test data.
71 var original_lp = window.LP
72 window.LP = {
73 links: {},
74 cache: {}
75 };
76
77 LP.cache.context = {
78 title: 'Test Project',
79 self_link: 'https://launchpad.dev/api/test_project'
80 };
81 LP.cache.administratedTeams = [];
82 LP.cache.importances = ['Unknown', 'Critical', 'High', 'Medium',
83 'Low', 'Wishlist', 'Undecided'];
84 LP.cache.statuses = ['New', 'Incomplete', 'Opinion',
85 'Invalid', 'Won\'t Fix', 'Expired',
86 'Confirmed', 'Triaged', 'In Progress',
87 'Fix Committed', 'Fix Released', 'Unknown'];
88 LP.links.me = 'https://launchpad.dev/api/~someone';
89 return original_lp;
90 }
91
92 function LPClient(){
93 if (!(this instanceof arguments.callee))
94 throw new Error("Constructor called as a function");
95 this.received = []
96 // We create new functions every time because we allow them to be
97 // configured.
98 this.named_post = function(url, func, config) {
99 this._call('named_post', config, arguments);
100 };
101 this.patch = function(bug_filter, data, config) {
102 this._call('patch', config, arguments);
103 }
104 };
105 LPClient.prototype._call = function(name, config, args) {
106 this.received.push(
107 [name, Array.prototype.slice.call(args)]);
108 if (!Y.Lang.isValue(args.callee.args))
109 throw new Error("Set call_args on "+name);
110 if (Y.Lang.isValue(args.callee.fail) && args.callee.fail) {
111 config.on.failure.apply(undefined, args.callee.args);
112 } else {
113 config.on.success.apply(undefined, args.callee.args);
114 }
115 };
116 // DELETE uses Y.io directly as of this writing, so we cannot stub it
117 // here.
118
119 function make_lp_client_stub() {
120 return new LPClient();
121 }
122
61 test_case = new Y.Test.Case({123 test_case = new Y.Test.Case({
62 name: 'structural_subscription_overlay',124 name: 'structural_subscription_overlay',
63125
@@ -467,28 +529,11 @@
467 setUp: function() {529 setUp: function() {
468 // Monkeypatch LP to avoid network traffic and to allow530 // Monkeypatch LP to avoid network traffic and to allow
469 // insertion of test data.531 // insertion of test data.
470 window.LP = {532 this.original_lp = monkeypatch_LP();
471 links: {},533
472 cache: {}
473 };
474
475 LP.cache.context = {
476 title: 'Test Project',
477 self_link: 'https://launchpad.dev/api/test_project'
478 };
479 LP.cache.administratedTeams = [];
480 LP.cache.importances = ['Unknown', 'Critical', 'High', 'Medium',
481 'Low', 'Wishlist', 'Undecided'];
482 LP.cache.statuses = ['New', 'Incomplete', 'Opinion',
483 'Invalid', 'Won\'t Fix', 'Expired',
484 'Confirmed', 'Triaged', 'In Progress',
485 'Fix Committed', 'Fix Released', 'Unknown'];
486 LP.links.me = 'https://launchpad.dev/api/~someone';
487
488 var lp_client = function() {};
489 this.configuration = {534 this.configuration = {
490 content_box: content_box_id,535 content_box: content_box_id,
491 lp_client: lp_client536 lp_client: make_lp_client_stub()
492 };537 };
493538
494 this.content_node = create_test_node();539 this.content_node = create_test_node();
@@ -496,17 +541,16 @@
496 },541 },
497542
498 tearDown: function() {543 tearDown: function() {
499 remove_test_node();544 window.LP = this.original_lp;
500 delete this.content_node;545 remove_test_node();
546 delete this.content_node;
501 },547 },
502548
503 test_overlay_error_handling_adding: function() {549 test_overlay_error_handling_adding: function() {
504 // Verify that errors generated during adding of a filter are550 // Verify that errors generated during adding of a filter are
505 // displayed to the user.551 // displayed to the user.
506 this.configuration.lp_client.named_post =552 this.configuration.lp_client.named_post.fail = true;
507 function(url, func, config) {553 this.configuration.lp_client.named_post.args = [true, true];
508 config.on.failure(true, true);
509 };
510 module.setup(this.configuration);554 module.setup(this.configuration);
511 module._show_add_overlay(this.configuration);555 module._show_add_overlay(this.configuration);
512 // After the setup the overlay should be in the DOM.556 // After the setup the overlay should be in the DOM.
@@ -526,17 +570,10 @@
526 // displayed to the user.570 // displayed to the user.
527 var original_delete_filter = module._delete_filter;571 var original_delete_filter = module._delete_filter;
528 module._delete_filter = function() {};572 module._delete_filter = function() {};
529 this.configuration.lp_client.patch =573 this.configuration.lp_client.patch.fail = true;
530 function(bug_filter, data, config) {574 this.configuration.lp_client.patch.args = [true, true];
531 config.on.failure(true, true);575 this.configuration.lp_client.named_post.args = [
532 };576 {'getAttrs': function() { return {}; }}];
533 var bug_filter = {
534 'getAttrs': function() { return {}; }
535 };
536 this.configuration.lp_client.named_post =
537 function(url, func, config) {
538 config.on.success(bug_filter);
539 };
540 module.setup(this.configuration);577 module.setup(this.configuration);
541 module._show_add_overlay(this.configuration);578 module._show_add_overlay(this.configuration);
542 // After the setup the overlay should be in the DOM.579 // After the setup the overlay should be in the DOM.
@@ -794,6 +831,131 @@
794831
795 }));832 }));
796833
834 suite.add(new Y.Test.Case({
835 name: 'Structural Subscription mute team subscriptions',
836
837 // Verify that the mute controls and labels on the edit block
838 // render and interact properly
839
840 _should: {
841 error: {
842 }
843 },
844
845 setUp: function() {
846 // Monkeypatch LP to avoid network traffic and to allow
847 // insertion of test data.
848 this.original_lp = monkeypatch_LP();
849 this.test_node = create_test_node(true);
850 Y.one('body').appendChild(this.test_node);
851 this.lp_client = make_lp_client_stub();
852 LP.cache.subscription_info = [
853 {target_url: 'http://example.com',
854 target_title:'Example project',
855 filters: [
856 {filter: {
857 statuses: [],
858 importances: [],
859 tags: [],
860 find_all_tags: true,
861 bug_notification_level: 'Discussion',
862 self_link: 'http://example.com/a_filter'
863 },
864 can_mute: true,
865 is_muted: false,
866 subscriber_is_team: true,
867 subscriber_url: 'http://example.com/subscriber',
868 subscriber_title: 'Thidwick',
869 user_is_team_admin: false,
870 }
871 ]
872 }
873 ]
874 },
875
876 tearDown: function() {
877 remove_test_node();
878 window.LP = this.original_lp;
879 },
880
881 test_not_muted_rendering: function() {
882 // Verify that an unmuted subscription is rendered correctly.
883 module.setup_bug_subscriptions(
884 {content_box: content_box_id,
885 lp_client: this.lp_client});
886 var listing = this.test_node.one(subscription_listing_id);
887 var filter_node = listing.one('#subscription-filter-0');
888 Assert.isNotNull(filter_node);
889 var mute_label_node = filter_node.one('.mute-label');
890 Assert.isNotNull(mute_label_node);
891 Assert.areEqual(mute_label_node.getStyle('display'), 'none');
892 var mute_link = filter_node.one('a.mute-subscription');
893 Assert.isNotNull(mute_link);
894 Assert.isTrue(mute_link.hasClass('no'));
895 },
896
897 test_muted_rendering: function() {
898 // Verify that a muted subscription is rendered correctly.
899 LP.cache.subscription_info[0].filters[0].is_muted = true;
900 module.setup_bug_subscriptions(
901 {content_box: content_box_id,
902 lp_client: this.lp_client});
903 var listing = this.test_node.one(subscription_listing_id);
904 var filter_node = listing.one('#subscription-filter-0');
905 Assert.isNotNull(filter_node);
906 var mute_label_node = filter_node.one('.mute-label');
907 Assert.isNotNull(mute_label_node);
908 Assert.areEqual(mute_label_node.getStyle('display'), 'inline');
909 var mute_link = filter_node.one('a.mute-subscription');
910 Assert.isNotNull(mute_link);
911 Assert.isTrue(mute_link.hasClass('yes'));
912 },
913
914 test_not_muted_toggle_muted: function() {
915 // Verify that an unmuted subscription can be muted.
916 module.setup_bug_subscriptions(
917 {content_box: content_box_id,
918 lp_client: this.lp_client});
919 var listing = this.test_node.one(subscription_listing_id);
920 var filter_node = listing.one('#subscription-filter-0');
921 var mute_label_node = filter_node.one('.mute-label');
922 var mute_link = filter_node.one('a.mute-subscription');
923 this.lp_client.named_post.args = []
924 Y.Event.simulate(Y.Node.getDOMNode(mute_link), 'click');
925 Assert.areEqual(this.lp_client.received[0][0], 'named_post');
926 Assert.areEqual(
927 this.lp_client.received[0][1][0],
928 'http://example.com/a_filter');
929 Assert.areEqual(
930 this.lp_client.received[0][1][1], 'mute');
931 Assert.areEqual(mute_label_node.getStyle('display'), 'inline');
932 Assert.isTrue(mute_link.hasClass('yes'));
933 },
934
935 test_muted_toggle_not_muted: function() {
936 // Verify that an muted subscription can be unmuted.
937 LP.cache.subscription_info[0].filters[0].is_muted = true;
938 module.setup_bug_subscriptions(
939 {content_box: content_box_id,
940 lp_client: this.lp_client});
941 var listing = this.test_node.one(subscription_listing_id);
942 var filter_node = listing.one('#subscription-filter-0');
943 var mute_label_node = filter_node.one('.mute-label');
944 var mute_link = filter_node.one('a.mute-subscription');
945 this.lp_client.named_post.args = []
946 Y.Event.simulate(Y.Node.getDOMNode(mute_link), 'click');
947 Assert.areEqual(this.lp_client.received[0][0], 'named_post');
948 Assert.areEqual(
949 this.lp_client.received[0][1][0],
950 'http://example.com/a_filter');
951 Assert.areEqual(
952 this.lp_client.received[0][1][1], 'unmute');
953 Assert.areEqual(mute_label_node.getStyle('display'), 'none');
954 Assert.isTrue(mute_link.hasClass('no'));
955 }
956
957 }));
958
797 // Lock, stock, and two smoking barrels.959 // Lock, stock, and two smoking barrels.
798 var handle_complete = function(data) {960 var handle_complete = function(data) {
799 var status_node = Y.Node.create(961 var status_node = Y.Node.create(

Subscribers

People subscribed via source and target branches

to status/vote changes: