Merge lp:~benji/launchpad/bug-777786 into lp:launchpad
- bug-777786
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | code | Approve | |
Review via email:
|
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/
The make lint report is clean.
The tests are in lib/lp/
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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> </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: {}}, |