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
=== modified file 'lib/lp/bugs/browser/structuralsubscription.py'
--- lib/lp/bugs/browser/structuralsubscription.py 2011-05-10 15:16:05 +0000
+++ lib/lp/bugs/browser/structuralsubscription.py 2011-05-18 14:32:15 +0000
@@ -14,7 +14,10 @@
14 'StructuralSubscribersPortletView',14 'StructuralSubscribersPortletView',
15 ]15 ]
1616
17from operator import attrgetter17from operator import (
18 attrgetter,
19 itemgetter,
20 )
1821
19from lazr.restful.interfaces import (22from lazr.restful.interfaces import (
20 IJSONRequestCache,23 IJSONRequestCache,
@@ -491,20 +494,33 @@
491 subscriber = subscription.subscriber494 subscriber = subscription.subscriber
492 for filter in subscription.bug_filters:495 for filter in subscription.bug_filters:
493 is_team = subscriber.isTeam()496 is_team = subscriber.isTeam()
494 user_is_team_admin = (is_team and497 user_is_team_admin = (
495 subscriber in administered_teams)498 is_team and subscriber in administered_teams)
499 team_has_contact_address = (
500 is_team and subscriber.preferredemail is not None)
501 mailing_list = subscriber.mailing_list
502 user_is_on_team_mailing_list = (
503 team_has_contact_address and
504 mailing_list is not None and
505 mailing_list.is_usable and
506 mailing_list.getSubscription(subscriber) is not None)
496 record['filters'].append(dict(507 record['filters'].append(dict(
497 filter=filter,508 filter=filter,
498 subscriber_link=absoluteURL(subscriber, api_request),509 subscriber_link=absoluteURL(subscriber, api_request),
499 subscriber_url = canonical_url(510 subscriber_url=canonical_url(
500 subscriber, rootsite='mainsite'),511 subscriber, rootsite='mainsite'),
512 target_bugs_url=canonical_url(
513 target, rootsite='bugs'),
501 subscriber_title=subscriber.title,514 subscriber_title=subscriber.title,
502 subscriber_is_team=is_team,515 subscriber_is_team=is_team,
503 user_is_team_admin=user_is_team_admin,516 user_is_team_admin=user_is_team_admin,
517 team_has_contact_address=team_has_contact_address,
518 user_is_on_team_mailing_list=user_is_on_team_mailing_list,
504 can_mute=filter.isMuteAllowed(user),519 can_mute=filter.isMuteAllowed(user),
505 is_muted=filter.muted(user) is not None))520 is_muted=filter.muted(user) is not None,
521 target_title=target.title))
506 info = info.values()522 info = info.values()
507 info.sort(key=lambda item: item['target_url'])523 info.sort(key=itemgetter('target_url'))
508 IJSONRequestCache(request).objects['subscription_info'] = info524 IJSONRequestCache(request).objects['subscription_info'] = info
509525
510526
511527
=== modified file 'lib/lp/bugs/model/bugsubscriptionfilter.py'
--- lib/lp/bugs/model/bugsubscriptionfilter.py 2011-04-05 22:34:35 +0000
+++ lib/lp/bugs/model/bugsubscriptionfilter.py 2011-05-18 14:32:15 +0000
@@ -255,9 +255,15 @@
255255
256 def isMuteAllowed(self, person):256 def isMuteAllowed(self, person):
257 """See `IBugSubscriptionFilter`."""257 """See `IBugSubscriptionFilter`."""
258 subscriber = self.structural_subscription.subscriber
259 # The person can mute the Subscription if the subscription is via a
260 # team of which they are a member and the team doesn't have a contact
261 # address (because if the team does, then the mute would be
262 # ineffectual).
258 return (263 return (
259 self.structural_subscription.subscriber.isTeam() and264 subscriber.isTeam() and
260 person.inTeam(self.structural_subscription.subscriber))265 person.inTeam(subscriber) and
266 subscriber.preferredemail is None)
261267
262 def muted(self, person):268 def muted(self, person):
263 store = Store.of(self)269 store = Store.of(self)
264270
=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
--- lib/lp/registry/interfaces/mailinglist.py 2011-05-12 21:33:10 +0000
+++ lib/lp/registry/interfaces/mailinglist.py 2011-05-18 14:32:15 +0000
@@ -110,7 +110,7 @@
110 INACTIVE = DBItem(7, """110 INACTIVE = DBItem(7, """
111 Inactive111 Inactive
112112
113 A previously active mailing lit has been made inactive by its team113 A previously active mailing list has been made inactive by its team
114 owner.114 owner.
115 """)115 """)
116116
@@ -132,7 +132,7 @@
132132
133 The mailing list has been flagged for deactivation by the team owner.133 The mailing list has been flagged for deactivation by the team owner.
134 Mailman will be informed of this and will take the necessary actions134 Mailman will be informed of this and will take the necessary actions
135 to deactive the list.135 to deactivate the list.
136 """)136 """)
137137
138 MOD_FAILED = DBItem(11, """138 MOD_FAILED = DBItem(11, """
@@ -246,8 +246,7 @@
246 title=_('Review date'),246 title=_('Review date'),
247 description=_('The date on which this mailing list registration was '247 description=_('The date on which this mailing list registration was '
248 'reviewed, or None if the registration has not yet '248 'reviewed, or None if the registration has not yet '
249 'been reviewed.')249 'been reviewed.'))
250 )
251250
252 date_activated = Datetime(251 date_activated = Datetime(
253 title=_('Activation date'),252 title=_('Activation date'),
@@ -255,8 +254,7 @@
255 'meaning that the Mailman process has successfully '254 'meaning that the Mailman process has successfully '
256 'created it. This may be None if the mailing list '255 'created it. This may be None if the mailing list '
257 'has not yet been activated, or that its activation '256 'has not yet been activated, or that its activation '
258 'has failed.')257 'has failed.'))
259 )
260258
261 status = Choice(259 status = Choice(
262 title=_('Status'),260 title=_('Status'),
@@ -434,8 +432,8 @@
434 def getReviewableMessages(message_id_filter=None):432 def getReviewableMessages(message_id_filter=None):
435 """Return the set of all held messages for this list requiring review.433 """Return the set of all held messages for this list requiring review.
436434
437 :param message_id_filter: If supplied only messages with message ids in435 :param message_id_filter: If supplied only messages with message ids
438 the filter are returned.436 in the filter are returned.
439 :return: A sequence of `IMessageApproval`s for this mailing list,437 :return: A sequence of `IMessageApproval`s for this mailing list,
440 where the status is `PostedMessageStatus.NEW`. The returned set438 where the status is `PostedMessageStatus.NEW`. The returned set
441 is ordered first by the date the message was posted, then by439 is ordered first by the date the message was posted, then by
442440
=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js 2011-05-11 21:01:58 +0000
+++ lib/lp/registry/javascript/structural-subscription.js 2011-05-18 14:32:15 +0000
@@ -1239,7 +1239,7 @@
1239 } else {1239 } else {
1240 filter_info.is_muted = false;1240 filter_info.is_muted = false;
1241 }1241 }
1242 handle_mute(node, filter_info.is_muted);1242 handle_mute(node, filter_info);
1243 },1243 },
1244 failure: error_handler.getFailureHandler()1244 failure: error_handler.getFailureHandler()
1245 }1245 }
@@ -1304,11 +1304,11 @@
1304/**1304/**
1305 * For a given filter node, set it up properly based on mute state.1305 * For a given filter node, set it up properly based on mute state.
1306 */1306 */
1307function handle_mute(node, muted) {1307function handle_mute(node, filter_info) {
1308 var control = node.one('a.mute-subscription');1308 var control = node.one('a.mute-subscription');
1309 var label = node.one('em.mute-label');1309 var label = node.one('em.mute-label');
1310 var description = node.one('.filter-description');1310 var description = node.one('.filter-description');
1311 if (muted) {1311 if (filter_info.is_muted) {
1312 control.set('text', 'Send my emails for this subscription');1312 control.set('text', 'Send my emails for this subscription');
1313 control.replaceClass(MUTE_ICON_CLASS, UNMUTE_ICON_CLASS);1313 control.replaceClass(MUTE_ICON_CLASS, UNMUTE_ICON_CLASS);
1314 label.setStyle('display', null);1314 label.setStyle('display', null);
@@ -1335,23 +1335,31 @@
1335 '<strong class="filter-name"></strong>'));1335 '<strong class="filter-name"></strong>'));
13361336
1337 if (filter_info.can_mute) {1337 if (filter_info.can_mute) {
1338 filter_node.appendChild(Y.Node.create(1338 filter_node.append(Y.Node.create('<em/>')
1339 '<em class="mute-label" style="padding-left: 1em;">You '+1339 .set('text', 'You do not receive emails from this subscription.')
1340 'do not receive emails from this subscription.</em>'));1340 .addClass('mute-label')
1341 .setStyle('paddingLeft', '1em'));
1341 }1342 }
13421343
1343 var control = filter_node.appendChild(1344 var control = filter_node.appendChild(
1344 Y.Node.create('<span style="float: right"></span>'));1345 Y.Node.create('<span style="float: right"></span>'));
13451346
1346 if (filter_info.can_mute) {1347 if (filter_info.can_mute) {
1347 var link = control.appendChild(Y.Node.create(1348 var link = control.appendChild(Y.Node.create('<a/>')
1348 '<a href="#" class="sprite js-action mute-subscription"></a>'));1349 .set('href', '#')
1349 var help = control.appendChild(Y.Node.create(1350 .addClass('sprite')
1350 '<a target="help" class="sprite maybe mute-help"'+1351 .addClass('js-action')
1351 ' style="visibility: hidden;"'+1352 .addClass('mute-subscription'));
1352 ' href="/+help/structural-subscription-mute.html">'+1353 var help = control.appendChild(Y.Node.create('<a/>')
1353 ' <span class="invisible-link">Delivery help</span>'+1354 .set('href', '/+help/structural-subscription-mute.html')
1354 '</a>'));1355 .set('target', 'help')
1356 .addClass('sprite')
1357 .addClass('maybe')
1358 .addClass('mute-help')
1359 .setStyle('visibility', 'hidden')
1360 .append(Y.Node.create('<span/>')
1361 .addClass('invisible-link')
1362 .set('text', 'Delivery help')));
1355 // For some reason the help link will not appear in Chrome unless1363 // For some reason the help link will not appear in Chrome unless
1356 // there is a non-empty element immediately after the help node.1364 // there is a non-empty element immediately after the help node.
1357 control.append(Y.Node.create('<span>&nbsp;</span>'));1365 control.append(Y.Node.create('<span>&nbsp;</span>'));
@@ -1362,7 +1370,7 @@
1362 help.setStyle('visibility', 'visible');1370 help.setStyle('visibility', 'visible');
1363 // Every time we trigger the display of the help link we need to1371 // Every time we trigger the display of the help link we need to
1364 // cancel any pending hiding of the help link so it doesn't1372 // cancel any pending hiding of the help link so it doesn't
1365 // dissapear on us. If there isn't one pending, this is a NOP.1373 // disappear on us. If there isn't one pending, this is a NOP.
1366 clearTimeout(hide_help_timeout);1374 clearTimeout(hide_help_timeout);
1367 };1375 };
1368 var hide_help = function () {1376 var hide_help = function () {
@@ -1376,20 +1384,56 @@
13761384
1377 if (can_edit(filter_info)) {1385 if (can_edit(filter_info)) {
1378 // User can edit the subscription.1386 // User can edit the subscription.
1379 control.append(Y.Node.create(1387 control.append(Y.Node.create('<a/>')
1380 '<a href="#" style="margin-right: 2em;"'+1388 .set('href', '#')
1381 ' class="sprite modify edit js-action edit-subscription">'+1389 .set('text', 'Edit this subscription')
1382 ' Edit this subscription</a>'+1390 .setStyle('marginRight', '2em')
1383 '<a href="#" class="sprite modify remove js-action '+1391 .addClass('sprite')
1384 ' delete-subscription">Unsubscribe</a>'));1392 .addClass('modify')
1385 }1393 .addClass('edit')
13861394 .addClass('js-action')
1387 filter_node.appendChild(1395 .addClass('edit-subscription'));
1388 Y.Node.create('<div style="padding-left: 1em" '+1396 control.append(Y.Node.create('<a/>')
1389 'class="filter-description"></div>'));1397 .set('href', '#')
1398 .set('text', 'Unsubscribe')
1399 .addClass('sprite')
1400 .addClass('modify')
1401 .addClass('remove')
1402 .addClass('js-action')
1403 .addClass('delete-subscription'));
1404 }
1405
1406 if (filter_info.team_has_contact_address
1407 && !filter_info.user_is_team_admin) {
1408 var subject = urlEncode('Team contact address and subscriptions');
1409 var user_participation;
1410 if (filter_info.user_is_on_team_mailing_list) {
1411 user_participation = 'subscribe to the team\'s mailing list';
1412 } else {
1413 user_participation = 'be a part of the team';
1414 }
1415 var message = urlEncode(
1416 'Hello. I receive email notifications about bugs in '+
1417 filter_info.target_title+' because of a team subscription for '+
1418 filter_info.subscriber_title+'. I would like to continue to '+
1419 user_participation+', but I would like to receive less email '+
1420 'from this subscription. Could you remove the team contact '+
1421 'email address so that the team members can manage their own '+
1422 'subscriptions (see '+filter_info.subscriber_url+'), or delete '+
1423 'or reduce level of the subscription itself (see '+
1424 filter_info.target_bugs_url+'/+subscriptions)?\n\nThank you.');
1425 control.append(Y.Node.create('<a/>')
1426 .set('href', filter_info.subscriber_url+'/+contactuser'+
1427 '?field.message='+message+'&field.subject='+subject)
1428 .set('text', 'Request team administrators change'));
1429 }
1430
1431 filter_node.append(Y.Node.create('<div/>')
1432 .setStyle('paddingLeft', '1em')
1433 .addClass('filter-description'));
13901434
1391 if (filter_info.can_mute) {1435 if (filter_info.can_mute) {
1392 handle_mute(filter_node, filter_info.is_muted);1436 handle_mute(filter_node, filter_info);
1393 }1437 }
13941438
1395 fill_filter_description(filter_node, filter_info, filter);1439 fill_filter_description(filter_node, filter_info, filter);
@@ -1397,6 +1441,10 @@
1397 return filter_node;1441 return filter_node;
1398}1442}
13991443
1444// Expose in the namespace for testing purposes.
1445namespace._create_filter_node = create_filter_node;
1446
1447
1400/**1448/**
1401 * Create a node with subscription description.1449 * Create a node with subscription description.
1402 */1450 */
@@ -1424,7 +1472,7 @@
1424 // and see the information you expect to see.1472 // and see the information you expect to see.
1425 LP.cache['structural-subscription-filter-'+filter_id.toString()] =1473 LP.cache['structural-subscription-filter-'+filter_id.toString()] =
1426 filter;1474 filter;
1427 node.appendChild(create_filter_node(filter_id, filter_info, filter));1475 node.append(create_filter_node(filter_id, filter_info, filter));
1428 filter_id += 1;1476 filter_id += 1;
1429 }1477 }
1430 return node;1478 return node;
@@ -1641,7 +1689,7 @@
1641 var team_info = get_team_info(form_data);1689 var team_info = get_team_info(form_data);
16421690
1643 var filter_info = {1691 var filter_info = {
1644 filter : filter,1692 filter: filter,
1645 subscriber_is_team: team_info.is_team,1693 subscriber_is_team: team_info.is_team,
1646 user_is_team_admin: team_info.is_team,1694 user_is_team_admin: team_info.is_team,
1647 can_mute: team_info.is_team,1695 can_mute: team_info.is_team,
16481696
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-05-11 21:05:17 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2011-05-18 14:32:15 +0000
@@ -716,6 +716,160 @@
716 suite.add(test_case);716 suite.add(test_case);
717717
718 suite.add(new Y.Test.Case({718 suite.add(new Y.Test.Case({
719 name: 'Structural Subscription team contact',
720
721 _should: {error: {}},
722
723 setUp: function() {
724 // Monkeypatch LP to avoid network traffic and to allow
725 // insertion of test data.
726 this.original_lp = monkeypatch_LP();
727
728 LP.cache.subscription_info = [{
729 target_url: 'http://example.com',
730 target_title:'Example project',
731 filters: [{
732 filter: {
733 description: 'DESCRIPTION',
734 statuses: [],
735 importances: [],
736 tags: [],
737 find_all_tags: true,
738 bug_notification_level: 'Discussion',
739 self_link: 'http://example.com/a_filter'
740 },
741 can_mute: true,
742 is_muted: false,
743 team_has_contact_address: true,
744 user_is_team_admin: false,
745 subscriber_is_team: true,
746 subscriber_url: 'http://example.com/subscriber',
747 subscriber_title: 'Thidwick'
748 }]
749 }];
750 this.configuration = {
751 content_box: content_box_id,
752 lp_client: make_lp_client_stub()
753 };
754
755 this.content_node = create_test_node();
756 Y.one('body').appendChild(this.content_node);
757 },
758
759 tearDown: function() {
760 window.LP = this.original_lp;
761 remove_test_node();
762 delete this.content_node;
763 },
764
765 test_administrative_change_link: function() {
766 var filter_info = {
767 filter: {
768 description: 'DESCRIPTION',
769 statuses: [],
770 importances: [],
771 tags: [],
772 find_all_tags: true,
773 bug_notification_level: 'Discussion',
774 self_link: 'http://example.com/a_filter'
775 },
776 can_mute: true,
777 is_muted: false,
778 team_has_contact_address: true,
779 user_is_team_admin: false,
780 user_is_on_team_mailing_list: true,
781 subscriber_is_team: true,
782 subscriber_url: 'http://example.com/subscriber',
783 subscriber_title: 'Thidwick'
784 };
785 var node = module._create_filter_node(
786 'ID', filter_info, filter_info.filter);
787 var content = node.getContent();
788 // If a subscription is via a team and the user isn't a team
789 // admin and the team has a contact address, the user gets a link
790 // to request the administrators change the subscription or drop
791 // the contact address.
792 Assert.areNotEqual(
793 -1,
794 content.search(/Request team administrators change/));
795 // If the team's contact address is to a (launchpad-managed)
796 // mailing list, then the pre-filled in email message is phrased
797 // accordingly.
798 Assert.areNotEqual(
799 -1,
800 content.search(/subscribe%20to%20the%20team/));
801 },
802
803 test_administrative_change_link_no_mailing_list: function() {
804 var filter_info = {
805 filter: {
806 description: 'DESCRIPTION',
807 statuses: [],
808 importances: [],
809 tags: [],
810 find_all_tags: true,
811 bug_notification_level: 'Discussion',
812 self_link: 'http://example.com/a_filter'
813 },
814 can_mute: true,
815 is_muted: false,
816 team_has_contact_address: true,
817 user_is_team_admin: false,
818 user_is_on_team_mailing_list: false,
819 subscriber_is_team: true,
820 subscriber_url: 'http://example.com/subscriber',
821 subscriber_title: 'Thidwick'
822 };
823 var node = module._create_filter_node(
824 'ID', filter_info, filter_info.filter);
825 var content = node.getContent();
826 // If a subscription is via a team and the user isn't a team
827 // admin and the team has a contact address, the user gets a link
828 // to request the administrators change the subscription or drop
829 // the contact address.
830 Assert.areNotEqual(
831 -1,
832 content.search(/Request team administrators change/));
833 // If the team's contact address is not a (launchpad-managed)
834 // mailing list, then the pre-filled in email message is phrased
835 // accordingly.
836 Assert.areNotEqual(
837 -1,
838 content.search(/be%20a%20part%20of%20the%20team/));
839 },
840
841 test_mute_not_shown_when_ineffectual: function() {
842 // If muting the subscription in question won't have an effect,
843 // then the mute link isn't shown.
844 var filter_info = {
845 filter: {
846 description: 'DESCRIPTION',
847 statuses: [],
848 importances: [],
849 tags: [],
850 find_all_tags: true,
851 bug_notification_level: 'Discussion',
852 self_link: 'http://example.com/a_filter'
853 },
854 can_mute: false,
855 is_muted: false,
856 team_has_contact_address: true,
857 user_is_team_admin: false,
858 user_is_on_team_mailing_list: true,
859 subscriber_is_team: true,
860 subscriber_url: 'http://example.com/subscriber',
861 subscriber_title: 'Thidwick'
862 };
863 var node = module._create_filter_node(
864 'ID', filter_info, filter_info.filter);
865 var content = node.getContent();
866 Assert.areEqual(
867 -1,
868 content.search(/Stop my emails/));
869 }
870 }));
871
872 suite.add(new Y.Test.Case({
719 name: 'Structural Subscription: deleting failed filters',873 name: 'Structural Subscription: deleting failed filters',
720874
721 _should: {error: {}},875 _should: {error: {}},