Merge lp:~benji/launchpad/bug-777786 into lp:launchpad

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: no longer in the source branch.
Merged at revision: 13079
Proposed branch: lp:~benji/launchpad/bug-777786
Merge into: lp:launchpad
Diff against target: 471 lines (+267/-45)
5 files modified
lib/lp/bugs/browser/structuralsubscription.py (+22/-6)
lib/lp/bugs/model/bugsubscriptionfilter.py (+8/-2)
lib/lp/registry/interfaces/mailinglist.py (+6/-8)
lib/lp/registry/javascript/structural-subscription.js (+77/-29)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+154/-0)
To merge this branch: bzr merge lp:~benji/launchpad/bug-777786
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+61274@code.launchpad.net

Commit message

[r=gmb][bug=777786] instead of an ineffectual "stop email" link, give users the option to contact the subscribing team

Description of the change

Bug 777786 notes that it's somewhat rude to show users a "Stop my emails
from this subscription" for subscriptions via teams that have a contact
address because it won't actually stop the email. This branch replaces
that link with a link to contact the team with a pre-populated message
that the user can edit.

I also fixed a bit of lint in lib/lp/registry/interfaces/mailinglist.py.

The make lint report is clean.

The tests are in lib/lp/registry/javascript/tests/test_structural_subscription.js.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
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/structuralsubscription.py'
2--- lib/lp/bugs/browser/structuralsubscription.py 2011-05-10 15:16:05 +0000
3+++ lib/lp/bugs/browser/structuralsubscription.py 2011-05-18 14:32:15 +0000
4@@ -14,7 +14,10 @@
5 'StructuralSubscribersPortletView',
6 ]
7
8-from operator import attrgetter
9+from operator import (
10+ attrgetter,
11+ itemgetter,
12+ )
13
14 from lazr.restful.interfaces import (
15 IJSONRequestCache,
16@@ -491,20 +494,33 @@
17 subscriber = subscription.subscriber
18 for filter in subscription.bug_filters:
19 is_team = subscriber.isTeam()
20- user_is_team_admin = (is_team and
21- subscriber in administered_teams)
22+ user_is_team_admin = (
23+ is_team and subscriber in administered_teams)
24+ team_has_contact_address = (
25+ is_team and subscriber.preferredemail is not None)
26+ mailing_list = subscriber.mailing_list
27+ user_is_on_team_mailing_list = (
28+ team_has_contact_address and
29+ mailing_list is not None and
30+ mailing_list.is_usable and
31+ mailing_list.getSubscription(subscriber) is not None)
32 record['filters'].append(dict(
33 filter=filter,
34 subscriber_link=absoluteURL(subscriber, api_request),
35- subscriber_url = canonical_url(
36+ subscriber_url=canonical_url(
37 subscriber, rootsite='mainsite'),
38+ target_bugs_url=canonical_url(
39+ target, rootsite='bugs'),
40 subscriber_title=subscriber.title,
41 subscriber_is_team=is_team,
42 user_is_team_admin=user_is_team_admin,
43+ team_has_contact_address=team_has_contact_address,
44+ user_is_on_team_mailing_list=user_is_on_team_mailing_list,
45 can_mute=filter.isMuteAllowed(user),
46- is_muted=filter.muted(user) is not None))
47+ is_muted=filter.muted(user) is not None,
48+ target_title=target.title))
49 info = info.values()
50- info.sort(key=lambda item: item['target_url'])
51+ info.sort(key=itemgetter('target_url'))
52 IJSONRequestCache(request).objects['subscription_info'] = info
53
54
55
56=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
57--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-05 22:34:35 +0000
58+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-05-18 14:32:15 +0000
59@@ -255,9 +255,15 @@
60
61 def isMuteAllowed(self, person):
62 """See `IBugSubscriptionFilter`."""
63+ subscriber = self.structural_subscription.subscriber
64+ # The person can mute the Subscription if the subscription is via a
65+ # team of which they are a member and the team doesn't have a contact
66+ # address (because if the team does, then the mute would be
67+ # ineffectual).
68 return (
69- self.structural_subscription.subscriber.isTeam() and
70- person.inTeam(self.structural_subscription.subscriber))
71+ subscriber.isTeam() and
72+ person.inTeam(subscriber) and
73+ subscriber.preferredemail is None)
74
75 def muted(self, person):
76 store = Store.of(self)
77
78=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
79--- lib/lp/registry/interfaces/mailinglist.py 2011-05-12 21:33:10 +0000
80+++ lib/lp/registry/interfaces/mailinglist.py 2011-05-18 14:32:15 +0000
81@@ -110,7 +110,7 @@
82 INACTIVE = DBItem(7, """
83 Inactive
84
85- A previously active mailing lit has been made inactive by its team
86+ A previously active mailing list has been made inactive by its team
87 owner.
88 """)
89
90@@ -132,7 +132,7 @@
91
92 The mailing list has been flagged for deactivation by the team owner.
93 Mailman will be informed of this and will take the necessary actions
94- to deactive the list.
95+ to deactivate the list.
96 """)
97
98 MOD_FAILED = DBItem(11, """
99@@ -246,8 +246,7 @@
100 title=_('Review date'),
101 description=_('The date on which this mailing list registration was '
102 'reviewed, or None if the registration has not yet '
103- 'been reviewed.')
104- )
105+ 'been reviewed.'))
106
107 date_activated = Datetime(
108 title=_('Activation date'),
109@@ -255,8 +254,7 @@
110 'meaning that the Mailman process has successfully '
111 'created it. This may be None if the mailing list '
112 'has not yet been activated, or that its activation '
113- 'has failed.')
114- )
115+ 'has failed.'))
116
117 status = Choice(
118 title=_('Status'),
119@@ -434,8 +432,8 @@
120 def getReviewableMessages(message_id_filter=None):
121 """Return the set of all held messages for this list requiring review.
122
123- :param message_id_filter: If supplied only messages with message ids in
124- the filter are returned.
125+ :param message_id_filter: If supplied only messages with message ids
126+ in the filter are returned.
127 :return: A sequence of `IMessageApproval`s for this mailing list,
128 where the status is `PostedMessageStatus.NEW`. The returned set
129 is ordered first by the date the message was posted, then by
130
131=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
132--- lib/lp/registry/javascript/structural-subscription.js 2011-05-11 21:01:58 +0000
133+++ lib/lp/registry/javascript/structural-subscription.js 2011-05-18 14:32:15 +0000
134@@ -1239,7 +1239,7 @@
135 } else {
136 filter_info.is_muted = false;
137 }
138- handle_mute(node, filter_info.is_muted);
139+ handle_mute(node, filter_info);
140 },
141 failure: error_handler.getFailureHandler()
142 }
143@@ -1304,11 +1304,11 @@
144 /**
145 * For a given filter node, set it up properly based on mute state.
146 */
147-function handle_mute(node, muted) {
148+function handle_mute(node, filter_info) {
149 var control = node.one('a.mute-subscription');
150 var label = node.one('em.mute-label');
151 var description = node.one('.filter-description');
152- if (muted) {
153+ if (filter_info.is_muted) {
154 control.set('text', 'Send my emails for this subscription');
155 control.replaceClass(MUTE_ICON_CLASS, UNMUTE_ICON_CLASS);
156 label.setStyle('display', null);
157@@ -1335,23 +1335,31 @@
158 '<strong class="filter-name"></strong>'));
159
160 if (filter_info.can_mute) {
161- filter_node.appendChild(Y.Node.create(
162- '<em class="mute-label" style="padding-left: 1em;">You '+
163- 'do not receive emails from this subscription.</em>'));
164+ filter_node.append(Y.Node.create('<em/>')
165+ .set('text', 'You do not receive emails from this subscription.')
166+ .addClass('mute-label')
167+ .setStyle('paddingLeft', '1em'));
168 }
169
170 var control = filter_node.appendChild(
171 Y.Node.create('<span style="float: right"></span>'));
172
173 if (filter_info.can_mute) {
174- var link = control.appendChild(Y.Node.create(
175- '<a href="#" class="sprite js-action mute-subscription"></a>'));
176- var help = control.appendChild(Y.Node.create(
177- '<a target="help" class="sprite maybe mute-help"'+
178- ' style="visibility: hidden;"'+
179- ' href="/+help/structural-subscription-mute.html">'+
180- ' <span class="invisible-link">Delivery help</span>'+
181- '</a>'));
182+ var link = control.appendChild(Y.Node.create('<a/>')
183+ .set('href', '#')
184+ .addClass('sprite')
185+ .addClass('js-action')
186+ .addClass('mute-subscription'));
187+ var help = control.appendChild(Y.Node.create('<a/>')
188+ .set('href', '/+help/structural-subscription-mute.html')
189+ .set('target', 'help')
190+ .addClass('sprite')
191+ .addClass('maybe')
192+ .addClass('mute-help')
193+ .setStyle('visibility', 'hidden')
194+ .append(Y.Node.create('<span/>')
195+ .addClass('invisible-link')
196+ .set('text', 'Delivery help')));
197 // For some reason the help link will not appear in Chrome unless
198 // there is a non-empty element immediately after the help node.
199 control.append(Y.Node.create('<span>&nbsp;</span>'));
200@@ -1362,7 +1370,7 @@
201 help.setStyle('visibility', 'visible');
202 // Every time we trigger the display of the help link we need to
203 // cancel any pending hiding of the help link so it doesn't
204- // dissapear on us. If there isn't one pending, this is a NOP.
205+ // disappear on us. If there isn't one pending, this is a NOP.
206 clearTimeout(hide_help_timeout);
207 };
208 var hide_help = function () {
209@@ -1376,20 +1384,56 @@
210
211 if (can_edit(filter_info)) {
212 // User can edit the subscription.
213- control.append(Y.Node.create(
214- '<a href="#" style="margin-right: 2em;"'+
215- ' class="sprite modify edit js-action edit-subscription">'+
216- ' Edit this subscription</a>'+
217- '<a href="#" class="sprite modify remove js-action '+
218- ' delete-subscription">Unsubscribe</a>'));
219- }
220-
221- filter_node.appendChild(
222- Y.Node.create('<div style="padding-left: 1em" '+
223- 'class="filter-description"></div>'));
224+ control.append(Y.Node.create('<a/>')
225+ .set('href', '#')
226+ .set('text', 'Edit this subscription')
227+ .setStyle('marginRight', '2em')
228+ .addClass('sprite')
229+ .addClass('modify')
230+ .addClass('edit')
231+ .addClass('js-action')
232+ .addClass('edit-subscription'));
233+ control.append(Y.Node.create('<a/>')
234+ .set('href', '#')
235+ .set('text', 'Unsubscribe')
236+ .addClass('sprite')
237+ .addClass('modify')
238+ .addClass('remove')
239+ .addClass('js-action')
240+ .addClass('delete-subscription'));
241+ }
242+
243+ if (filter_info.team_has_contact_address
244+ && !filter_info.user_is_team_admin) {
245+ var subject = urlEncode('Team contact address and subscriptions');
246+ var user_participation;
247+ if (filter_info.user_is_on_team_mailing_list) {
248+ user_participation = 'subscribe to the team\'s mailing list';
249+ } else {
250+ user_participation = 'be a part of the team';
251+ }
252+ var message = urlEncode(
253+ 'Hello. I receive email notifications about bugs in '+
254+ filter_info.target_title+' because of a team subscription for '+
255+ filter_info.subscriber_title+'. I would like to continue to '+
256+ user_participation+', but I would like to receive less email '+
257+ 'from this subscription. Could you remove the team contact '+
258+ 'email address so that the team members can manage their own '+
259+ 'subscriptions (see '+filter_info.subscriber_url+'), or delete '+
260+ 'or reduce level of the subscription itself (see '+
261+ filter_info.target_bugs_url+'/+subscriptions)?\n\nThank you.');
262+ control.append(Y.Node.create('<a/>')
263+ .set('href', filter_info.subscriber_url+'/+contactuser'+
264+ '?field.message='+message+'&field.subject='+subject)
265+ .set('text', 'Request team administrators change'));
266+ }
267+
268+ filter_node.append(Y.Node.create('<div/>')
269+ .setStyle('paddingLeft', '1em')
270+ .addClass('filter-description'));
271
272 if (filter_info.can_mute) {
273- handle_mute(filter_node, filter_info.is_muted);
274+ handle_mute(filter_node, filter_info);
275 }
276
277 fill_filter_description(filter_node, filter_info, filter);
278@@ -1397,6 +1441,10 @@
279 return filter_node;
280 }
281
282+// Expose in the namespace for testing purposes.
283+namespace._create_filter_node = create_filter_node;
284+
285+
286 /**
287 * Create a node with subscription description.
288 */
289@@ -1424,7 +1472,7 @@
290 // and see the information you expect to see.
291 LP.cache['structural-subscription-filter-'+filter_id.toString()] =
292 filter;
293- node.appendChild(create_filter_node(filter_id, filter_info, filter));
294+ node.append(create_filter_node(filter_id, filter_info, filter));
295 filter_id += 1;
296 }
297 return node;
298@@ -1641,7 +1689,7 @@
299 var team_info = get_team_info(form_data);
300
301 var filter_info = {
302- filter : filter,
303+ filter: filter,
304 subscriber_is_team: team_info.is_team,
305 user_is_team_admin: team_info.is_team,
306 can_mute: team_info.is_team,
307
308=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
309--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-05-11 21:05:17 +0000
310+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-05-18 14:32:15 +0000
311@@ -716,6 +716,160 @@
312 suite.add(test_case);
313
314 suite.add(new Y.Test.Case({
315+ name: 'Structural Subscription team contact',
316+
317+ _should: {error: {}},
318+
319+ setUp: function() {
320+ // Monkeypatch LP to avoid network traffic and to allow
321+ // insertion of test data.
322+ this.original_lp = monkeypatch_LP();
323+
324+ LP.cache.subscription_info = [{
325+ target_url: 'http://example.com',
326+ target_title:'Example project',
327+ filters: [{
328+ filter: {
329+ description: 'DESCRIPTION',
330+ statuses: [],
331+ importances: [],
332+ tags: [],
333+ find_all_tags: true,
334+ bug_notification_level: 'Discussion',
335+ self_link: 'http://example.com/a_filter'
336+ },
337+ can_mute: true,
338+ is_muted: false,
339+ team_has_contact_address: true,
340+ user_is_team_admin: false,
341+ subscriber_is_team: true,
342+ subscriber_url: 'http://example.com/subscriber',
343+ subscriber_title: 'Thidwick'
344+ }]
345+ }];
346+ this.configuration = {
347+ content_box: content_box_id,
348+ lp_client: make_lp_client_stub()
349+ };
350+
351+ this.content_node = create_test_node();
352+ Y.one('body').appendChild(this.content_node);
353+ },
354+
355+ tearDown: function() {
356+ window.LP = this.original_lp;
357+ remove_test_node();
358+ delete this.content_node;
359+ },
360+
361+ test_administrative_change_link: function() {
362+ var filter_info = {
363+ filter: {
364+ description: 'DESCRIPTION',
365+ statuses: [],
366+ importances: [],
367+ tags: [],
368+ find_all_tags: true,
369+ bug_notification_level: 'Discussion',
370+ self_link: 'http://example.com/a_filter'
371+ },
372+ can_mute: true,
373+ is_muted: false,
374+ team_has_contact_address: true,
375+ user_is_team_admin: false,
376+ user_is_on_team_mailing_list: true,
377+ subscriber_is_team: true,
378+ subscriber_url: 'http://example.com/subscriber',
379+ subscriber_title: 'Thidwick'
380+ };
381+ var node = module._create_filter_node(
382+ 'ID', filter_info, filter_info.filter);
383+ var content = node.getContent();
384+ // If a subscription is via a team and the user isn't a team
385+ // admin and the team has a contact address, the user gets a link
386+ // to request the administrators change the subscription or drop
387+ // the contact address.
388+ Assert.areNotEqual(
389+ -1,
390+ content.search(/Request team administrators change/));
391+ // If the team's contact address is to a (launchpad-managed)
392+ // mailing list, then the pre-filled in email message is phrased
393+ // accordingly.
394+ Assert.areNotEqual(
395+ -1,
396+ content.search(/subscribe%20to%20the%20team/));
397+ },
398+
399+ test_administrative_change_link_no_mailing_list: function() {
400+ var filter_info = {
401+ filter: {
402+ description: 'DESCRIPTION',
403+ statuses: [],
404+ importances: [],
405+ tags: [],
406+ find_all_tags: true,
407+ bug_notification_level: 'Discussion',
408+ self_link: 'http://example.com/a_filter'
409+ },
410+ can_mute: true,
411+ is_muted: false,
412+ team_has_contact_address: true,
413+ user_is_team_admin: false,
414+ user_is_on_team_mailing_list: false,
415+ subscriber_is_team: true,
416+ subscriber_url: 'http://example.com/subscriber',
417+ subscriber_title: 'Thidwick'
418+ };
419+ var node = module._create_filter_node(
420+ 'ID', filter_info, filter_info.filter);
421+ var content = node.getContent();
422+ // If a subscription is via a team and the user isn't a team
423+ // admin and the team has a contact address, the user gets a link
424+ // to request the administrators change the subscription or drop
425+ // the contact address.
426+ Assert.areNotEqual(
427+ -1,
428+ content.search(/Request team administrators change/));
429+ // If the team's contact address is not a (launchpad-managed)
430+ // mailing list, then the pre-filled in email message is phrased
431+ // accordingly.
432+ Assert.areNotEqual(
433+ -1,
434+ content.search(/be%20a%20part%20of%20the%20team/));
435+ },
436+
437+ test_mute_not_shown_when_ineffectual: function() {
438+ // If muting the subscription in question won't have an effect,
439+ // then the mute link isn't shown.
440+ var filter_info = {
441+ filter: {
442+ description: 'DESCRIPTION',
443+ statuses: [],
444+ importances: [],
445+ tags: [],
446+ find_all_tags: true,
447+ bug_notification_level: 'Discussion',
448+ self_link: 'http://example.com/a_filter'
449+ },
450+ can_mute: false,
451+ is_muted: false,
452+ team_has_contact_address: true,
453+ user_is_team_admin: false,
454+ user_is_on_team_mailing_list: true,
455+ subscriber_is_team: true,
456+ subscriber_url: 'http://example.com/subscriber',
457+ subscriber_title: 'Thidwick'
458+ };
459+ var node = module._create_filter_node(
460+ 'ID', filter_info, filter_info.filter);
461+ var content = node.getContent();
462+ Assert.areEqual(
463+ -1,
464+ content.search(/Stop my emails/));
465+ }
466+ }));
467+
468+ suite.add(new Y.Test.Case({
469 name: 'Structural Subscription: deleting failed filters',
470
471 _should: {error: {}},