← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~benji/launchpad/bug-784575-message into lp:launchpad

 

Benji York has proposed merging lp:~benji/launchpad/bug-784575-message into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~benji/launchpad/bug-784575-message/+merge/62157

Bug 784575 is about changing bug notification email to point to the new
bug +subscriptions page to manage subscriptions.  This branch does that.

The change itself (in bugnotification.py, bug-notification-verbose.txt,
and bug-notification.txt) is pretty simple.

The remainder of this branch is fixing tests to account for the new
message.  Instead of blindly substituting in the new message text I took
the time to understand the tests in question and tweak them so that in
many cases they no longer unneccesarily test for the parts of the
noficiation messages that aren't pertinant to the test in question.

Another large chunk of this branch is the pure lint fix of
xx-bug-personal-subscriptions.txt, fixing the test indentation.  No
other change was made to the file, but this prep will help a follow-on
branch.

I ran all of the lp.bugs tests to be sure nothing had broken:

    bin/test -c -m lp.bugs

Some lint is left:

./lib/lp/bugs/emailtemplates/bug-notification-verbose.txt
       3: Line has trailing whitespace.
./lib/lp/bugs/emailtemplates/bug-notification.txt
       3: Line has trailing whitespace.
./lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt
     274: want exceeds 78 characters.
     327: want exceeds 78 characters.

The bug notification templates have a trailing space on the signature
separator which is common and encouraged, so I'm leaving those (and it
occurs to me that they need a test... there I added one as
TestNotificationSignatureSeparator).

The two long lines in xx-bug-personal-subscriptions.txt are the best we
can do as far as I can tell.  If the original test author had written
down what they were testing with those assertions maybe I could do
better.

-- 
https://code.launchpad.net/~benji/launchpad/bug-784575-message/+merge/62157
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~benji/launchpad/bug-784575-message into lp:launchpad.
=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt	2011-05-18 13:00:11 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt	2011-05-24 16:07:40 +0000
@@ -76,12 +76,6 @@
     a comment.
     <BLANKLINE>
     ...
-    You received this bug notification because you are subscribed to
-    mozilla-firefox in Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
     ----------------------------------------------------------------------
     To: mark@xxxxxxxxxxx
     From: Sample Person <1@xxxxxxxxxxxxxxxxxx>
@@ -91,11 +85,6 @@
     a comment.
     <BLANKLINE>
     ...
-    You received this bug notification because you are a bug assignee.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
     ----------------------------------------------------------------------
     To: support@xxxxxxxxxx
     From: Sample Person <1@xxxxxxxxxxxxxxxxxx>
@@ -104,13 +93,7 @@
     <BLANKLINE>
     a comment.
     <BLANKLINE>
-    --
-    You received this bug notification because you are a member of Ubuntu
-    Team, which is the registrant for Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
+    ...
     ----------------------------------------------------------------------
     To: test@xxxxxxxxxxxxx
     From: Sample Person <1@xxxxxxxxxxxxxxxxxx>
@@ -120,14 +103,6 @@
     a comment.
     <BLANKLINE>
     ...
-    You received this bug notification because you are a direct subscriber
-    of the bug.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
-    <BLANKLINE>
-    ...
     ----------------------------------------------------------------------
 
 You can see that the message above contains the bug's initial comment's
@@ -192,13 +167,7 @@
     <BLANKLINE>
     a new comment.
     <BLANKLINE>
-    --
-    You received this bug notification because you are a member of Ubuntu
-    Team, which is the registrant for Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
+    ...
     ----------------------------------------------------------------------
     To: test@xxxxxxxxxxxxx
     ...
@@ -242,13 +211,8 @@
     <BLANKLINE>
     ** Visibility changed to: Private
     <BLANKLINE>
+    --
     ...
-    You received this bug notification because you are a member of Ubuntu
-    Team, which is the registrant for Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
     ----------------------------------------------------------------------
     To: test@xxxxxxxxxxxxx
     ...
@@ -300,12 +264,7 @@
     + Another summary
     <BLANKLINE>
     --
-    You received this bug notification because you are a member of Ubuntu
-    Team, which is the registrant for Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
+    ...
     ----------------------------------------------------------------------
     To: test@xxxxxxxxxxxxx
     ...
@@ -445,16 +404,7 @@
     <BLANKLINE>
     *** This bug is a duplicate of bug 1 ***
         http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    a comment.
-    <BLANKLINE>
-    -- 
-    You received this bug notification because you are a member of Ubuntu
-    Team, which is the registrant for Ubuntu.
-    http://bugs.launchpad.dev/bugs/16
-    <BLANKLINE>
-    Title:
-      new bug
+    ...
     ----------------------------------------------------------------------
     To: test@xxxxxxxxxxxxx
     From: Sample Person <16@xxxxxxxxxxxxxxxxxx>
@@ -463,19 +413,7 @@
     <BLANKLINE>
     *** This bug is a duplicate of bug 1 ***
         http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    a comment.
-    <BLANKLINE>
-    -- 
-    You received this bug notification because you are a direct subscriber
-    of the bug.
-    http://bugs.launchpad.dev/bugs/16
-    <BLANKLINE>
-    Title:
-      new bug
-    <BLANKLINE>
-    To unsubscribe from this bug, go to:
-    http://bugs.launchpad.dev/ubuntu/+bug/16/+subscribe
+    ...
     ----------------------------------------------------------------------
 
     >>> flush_notifications()
@@ -585,7 +523,8 @@
     ...         ten_minutes_ago, sample_person, "title",
     ...         True, False))
 
-    >>> notifications = getUtility(IBugNotificationSet).getNotificationsToSend()
+    >>> notifications = getUtility(
+    ...     IBugNotificationSet).getNotificationsToSend()
     >>> len(notifications)
     8
 
@@ -660,12 +599,7 @@
     <BLANKLINE>
     -- =
     <BLANKLINE>
-    You received this bug notification because you are subscribed to
-    mozilla-firefox in Ubuntu.
-    http://bugs.launchpad.dev/bugs/1
-    <BLANKLINE>
-    Title:
-      Firefox does not support SVG
+    You received this bug notification because...
     INFO    Notifying mark@xxxxxxxxxxx about bug 1.
     ...
     INFO    Notifying owner@xxxxxxxxxxx about bug 1.
@@ -1051,7 +985,7 @@
     >>> print_notification(collated_messages['concise@xxxxxxxxxxx'][0])
     To: concise@xxxxxxxxxxx
     ...
-    To unsubscribe from this bug, go to:...
+    To manage notifications about this bug go to:...
 
 Verbose Team Person gets a concise email, even though they belong to a team
 that gets verbose email.
@@ -1099,8 +1033,8 @@
        will be automatically wrapped by the BugNotification
        machinery. Ain't technology great?
     <BLANKLINE>
-    To unsubscribe from this bug, go to:
-    http://bugs.launchpad.dev/.../+bug/.../+subscribe
+    To manage notifications about this bug go to:
+    http://bugs.launchpad.dev/.../+bug/.../+subscriptions
     ----------------------------------------------------------------------
 
 And Concise Team Person does too, even though his team doesn't want them:
@@ -1128,6 +1062,9 @@
        This is a long description of the bug, which
        will be automatically wrapped by the BugNotification
        machinery. Ain't technology great?
+    <BLANKLINE>
+    To manage notifications about this bug go to:
+    http://bugs.launchpad.dev/.../+bug/.../+subscriptions
     ----------------------------------------------------------------------
 
 It's important to note that the bug title and description are wrapped
@@ -1143,7 +1080,7 @@
      ...
      'Bug description:',
      '  This is a long description of the bug, which will be automatically',
-     "  wrapped by the BugNotification machinery. Ain't technology great?"]
+     "  wrapped by the BugNotification machinery. Ain't technology great?"...]
 
 The title is also wrapped and indented in normal notifications.
 
@@ -1153,7 +1090,7 @@
     [...
      'Title:',
      '  In the beginning, the universe was created. This has made a lot of',
-     '  people very angry and has been widely regarded as a bad move']
+     '  people very angry and has been widely regarded as a bad move'...]
 
 Self-Generated Bug Notifications
 --------------------------------

=== modified file 'lib/lp/bugs/emailtemplates/bug-notification-verbose.txt'
--- lib/lp/bugs/emailtemplates/bug-notification-verbose.txt	2011-03-02 16:22:36 +0000
+++ lib/lp/bugs/emailtemplates/bug-notification-verbose.txt	2011-05-24 16:07:40 +0000
@@ -12,4 +12,4 @@
 Bug description:
 %(bug_description)s
 
-%(unsubscribe_notice)s
+%(subscriptions_message)s

=== modified file 'lib/lp/bugs/emailtemplates/bug-notification.txt'
--- lib/lp/bugs/emailtemplates/bug-notification.txt	2011-03-02 16:22:36 +0000
+++ lib/lp/bugs/emailtemplates/bug-notification.txt	2011-05-24 16:07:40 +0000
@@ -7,4 +7,4 @@
 Title:
 %(bug_title)s
 
-%(unsubscribe_notice)s
+%(subscriptions_message)s

=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py	2011-04-05 22:34:35 +0000
+++ lib/lp/bugs/scripts/bugnotification.py	2011-05-24 16:07:40 +0000
@@ -193,25 +193,26 @@
                 data['filter descriptions'])
         else:
             filters_text = u""
-        # XXX deryck 2009-11-17 Bug #484319
-        # This should be refactored to add a link inside the
-        # code where we build `reason`.  However, this will
-        # require some extra work, and this small change now
-        # will ease pain for a lot of unhappy users.
-        if 'direct subscriber' in reason and 'member of' not in reason:
-            unsubscribe_notice = ('To unsubscribe from this bug, go to:\n'
-                '%s/+subscribe' % canonical_url(bug.bugtasks[0]))
+
+        # In the rare case of a bug with no bugtasks, we can't generate the
+        # subscription management URL so just leave off the subscription
+        # management message entirely.
+        if len(bug.bugtasks):
+            bug_url = canonical_url(bug.bugtasks[0])
+            notification_url = bug_url + '/+subscriptions'
+            subscriptions_message = ('To manage notifications about this bug '
+                'go to:\n%s' % notification_url)
         else:
-            unsubscribe_notice = ''
+            subscriptions_message = ''
 
         data_wrapper = MailWrapper(width=72, indent='  ')
         body_data = {
             'content': mail_wrapper.format(content),
             'bug_title': data_wrapper.format(bug.title),
             'bug_url': canonical_url(bug),
-            'unsubscribe_notice': unsubscribe_notice,
             'notification_rationale': mail_wrapper.format(reason),
             'subscription_filters': filters_text,
+            'subscriptions_message': subscriptions_message,
             }
 
         # If the person we're sending to receives verbose notifications

=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
--- lib/lp/bugs/scripts/tests/test_bugnotification.py	2011-05-12 21:33:10 +0000
+++ lib/lp/bugs/scripts/tests/test_bugnotification.py	2011-05-24 16:07:40 +0000
@@ -929,7 +929,7 @@
 
     def setUp(self):
         super(TestEmailNotificationsWithFilters, self).setUp()
-        self.bug=self.factory.makeBug()
+        self.bug = self.factory.makeBug()
         subscriber = self.factory.makePerson()
         self.subscription = self.bug.default_bugtask.target.addSubscription(
             subscriber, subscriber)
@@ -1130,7 +1130,7 @@
         bug = self.product.createBug(params)
         notification = IStore(BugNotification).find(
             BugNotification,
-            BugNotification.id==BugNotificationRecipient.bug_notificationID,
+            BugNotification.id == BugNotificationRecipient.bug_notificationID,
             BugNotificationRecipient.personID == self.subscriber.id,
             BugNotification.bug == bug).one()
         self.assertEqual(notification.message.text_contents, message)
@@ -1145,7 +1145,42 @@
         bug = self.product.createBug(params)
         notifications = IStore(BugNotification).find(
             BugNotification,
-            BugNotification.id==BugNotificationRecipient.bug_notificationID,
+            BugNotification.id == BugNotificationRecipient.bug_notificationID,
             BugNotificationRecipient.personID == self.subscriber.id,
             BugNotification.bug == bug)
         self.assertTrue(notifications.is_empty())
+
+
+class TestManageNotificationsMessage(TestCaseWithFactory):
+    # See bug 784575.
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super(TestManageNotificationsMessage, self).setUp()
+        self.subscriber = self.factory.makePerson()
+        self.submitter = self.factory.makePerson()
+        self.product = self.factory.makeProduct(
+            bug_supervisor=self.submitter)
+        self.subscription = self.product.addSubscription(
+            self.subscriber, self.subscriber)
+        self.filter = self.subscription.bug_filters[0]
+        self.filter.description = u'Needs triage'
+        self.filter.statuses = [BugTaskStatus.NEW, BugTaskStatus.INCOMPLETE]
+
+    def test_manage_notifications_message_is_included(self):
+        message = u"this is a comment"
+        params = CreateBugParams(
+            title=u"crashes all the time",
+            comment=message, owner=self.submitter,
+            status=BugTaskStatus.NEW)
+        bug = self.product.createBug(params)
+        notification = IStore(BugNotification).find(
+            BugNotification,
+            BugNotification.id == BugNotificationRecipient.bug_notificationID,
+            BugNotificationRecipient.personID == self.subscriber.id,
+            BugNotification.bug == bug).one()
+        (message,) = construct_email_notifications([notification])[2]
+        payload = message.get_payload()
+        self.assertThat(payload, Contains(
+            'To manage notifications about this bug go to:\nhttp://'))

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt	2010-12-23 12:55:53 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt	2011-05-24 16:07:40 +0000
@@ -1,178 +1,177 @@
-= Personal Subscriptions =
+Personal Subscriptions
+======================
 
 Users can subscribe to bugs reported in Launchpad, via the "Subscribe" link
 in the actions portlet.
 
-  >>> from lp.bugs.tests.bug import (
-  ...     print_direct_subscribers, print_also_notified,
-  ...     print_subscribers_from_duplicates)
+    >>> from lp.bugs.tests.bug import (
+    ...     print_direct_subscribers, print_also_notified,
+    ...     print_subscribers_from_duplicates)
 
-  >>> browser = setupBrowser(auth='Basic foo.bar@xxxxxxxxxxxxx:test')
-  >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1'
+    >>> browser = setupBrowser(auth='Basic foo.bar@xxxxxxxxxxxxx:test')
+    >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1'
 
     >>> browser.getLink('Subscribe').click()
 
-  >>> subscription_widget = browser.getControl(name='field.subscription')
-  >>> subscription_widget.options
-  ['name16']
-  >>> subscription_widget.value
-  ['name16']
+    >>> subscription_widget = browser.getControl(name='field.subscription')
+    >>> subscription_widget.options
+    ['name16']
+    >>> subscription_widget.value
+    ['name16']
 
-  >>> submit = browser.getControl('Continue')
+    >>> submit = browser.getControl('Continue')
 
 Clicking "Continue" subscribes the user to the bug, and tells the user
 this.
 
-  >>> submit.click()
-
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1'
-
-  >>> for tag in find_tags_by_class(browser.contents, "informational message"):
-  ...   print tag.renderContents()
-  You have been subscribed to this bug.
+    >>> submit.click()
+
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1'
+
+    >>> tags = find_tags_by_class(browser.contents, "informational message")
+    >>> for tag in tags:
+    ...   print tag.renderContents()
+    You have been subscribed to this bug.
 
 There's also now a link to unsubscribe the user next to the name. It's a
 relative URL to +subscribe, so it will only work when the portlet is
 in the context of the bug page.
 
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> print_direct_subscribers(browser.contents)
-  Foo Bar (Self-subscribed) (Unsubscribe Foo Bar)
-  Sample Person (Subscribed by Launchpad Janitor)
-  Steve Alexander (Subscribed by Launchpad Janitor)
-
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> link = browser.getLink(id='unsubscribe-subscriber-16')
-  >>> print link.mech_link.url
-  +subscribe
-
-  >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/')
-  >>> browser.getLink('Unsubscribe').click()
-  >>> print browser.title
-  Bug #1...
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
+    >>> bug_1 = 'http://bugs.launchpad.dev/firefox/+bug/1/'
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> print_direct_subscribers(browser.contents)
+    Foo Bar (Self-subscribed) (Unsubscribe Foo Bar)
+    Sample Person (Subscribed by Launchpad Janitor)
+    Steve Alexander (Subscribed by Launchpad Janitor)
+
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> link = browser.getLink(id='unsubscribe-subscriber-16')
+    >>> print link.mech_link.url
+    +subscribe
+
+    >>> browser.open(bug_1)
+    >>> browser.getLink('Unsubscribe').click()
+    >>> print browser.title
+    Bug #1...
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
 
 
 Clicking the "Continue" button from the +subscribe page will unsubscribe
 the user this time, and inform the user.
 
-  >>> subscription_widget.value
-  ['name16']
-  >>> submit = browser.getControl('Continue')
-  >>> submit.click()
-
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/'
-
-  >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
-  ...   print tag.renderContents()
-  You have been unsubscribed from bug 1.
+    >>> subscription_widget.value
+    ['name16']
+    >>> submit = browser.getControl('Continue')
+    >>> submit.click()
+
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/'
+
+    >>> tags = find_tags_by_class(browser.contents, 'informational message')
+    >>> for tag in tags:
+    ...   print tag.renderContents()
+    You have been unsubscribed from bug 1.
 
 Users can unsubscribe teams to which they belong. Let's demonstrate by
 first subscribing one of Foo Bar's teams.
 
-  >>> browser.getLink('Subscribe someone else').click()
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber'
+    >>> browser.getLink('Subscribe someone else').click()
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber'
 
-  >>> browser.getControl('Person').value = 'launchpad'
-  >>> browser.getControl('Subscribe user').click()
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1'
+    >>> browser.getControl('Person').value = 'launchpad'
+    >>> browser.getControl('Subscribe user').click()
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1'
 
 There's an unsubscribe link next to the team name.
 
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> print_direct_subscribers(browser.contents)
-  Launchpad Developers (Subscribed by Foo Bar) (Unsubscribe Launchpad
-                                                Developers)
-  Sample Person (Subscribed by Launchpad Janitor)
-  Steve Alexander (Subscribed by Launchpad Janitor)
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> print_direct_subscribers(browser.contents)
+    Launchpad Developers (Subscribed by Foo Bar) (Unsubscribe Launchpad
+                                                    Developers)
+    Sample Person (Subscribed by Launchpad Janitor)
+    Steve Alexander (Subscribed by Launchpad Janitor)
 
 Clicking either the subscribe link (for subscribing the user to the bug)
 or the unsubscribe link for the team gives us the option of both
 subscribing Foo Bar, and unsubscribing the Launchpad team.
 
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> browser.getLink(id='unsubscribe-subscriber-57').mech_link.url
-  '+subscribe'
-
-  >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
-  >>> browser.getLink('Subscribe').click()
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
-
-  >>> subscription_widget = browser.getControl(name='field.subscription')
-  >>> subscription_widget.options
-  ['name16', 'launchpad']
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> browser.getLink(id='unsubscribe-subscriber-57').mech_link.url
+    '+subscribe'
+
+    >>> browser.open(bug_1)
+    >>> browser.getLink('Subscribe').click()
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
+
+    >>> subscription_widget = browser.getControl(name='field.subscription')
+    >>> subscription_widget.options
+    ['name16', 'launchpad']
 
 Let's unsubscribe the Launchpad team.
 
-  >>> subscription_widget.value = ['launchpad']
-  >>> browser.getControl('Continue').click()
-
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1'
-
-  >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
-  ...   print tag.renderContents()
-  Launchpad Developers has been unsubscribed from bug 1.
-
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> print_direct_subscribers(browser.contents)
-  Sample Person (Subscribed by Launchpad Janitor)
-  Steve Alexander (Subscribed by Launchpad Janitor)
+    >>> subscription_widget.value = ['launchpad']
+    >>> browser.getControl('Continue').click()
+
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1'
+
+    >>> tags = find_tags_by_class(browser.contents, 'informational message')
+    >>> for tag in tags:
+    ...   print tag.renderContents()
+    Launchpad Developers has been unsubscribed from bug 1.
+
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> print_direct_subscribers(browser.contents)
+    Sample Person (Subscribed by Launchpad Janitor)
+    Steve Alexander (Subscribed by Launchpad Janitor)
 
 On the subscribe page there's a Cancel link as well, that will return
 the browser to the bug page.
 
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/firefox/+bug/1/')
-  >>> browser.getLink('Subscribe').click()
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
+    >>> browser.open(bug_1)
+    >>> browser.getLink('Subscribe').click()
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
 
-  >>> subscription_widget = browser.getControl(name='field.subscription')
-  >>> subscription_widget.value
-  ['name16']
-  >>> browser.getLink('Cancel').click()
-  >>> browser.url
-  'http://bugs.launchpad.dev/firefox/+bug/1/'
+    >>> subscription_widget = browser.getControl(name='field.subscription')
+    >>> subscription_widget.value
+    ['name16']
+    >>> browser.getLink('Cancel').click()
+    >>> browser.url
+    'http://bugs.launchpad.dev/firefox/+bug/1/'
 
 Foo Bar wasn't subscribed to the bug.
 
-  >>> len(find_tags_by_class(browser.contents, 'informational message'))
-  0
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> print_direct_subscribers(browser.contents)
-  Sample Person (Subscribed by Launchpad Janitor)
-  Steve Alexander (Subscribed by Launchpad Janitor)
+    >>> len(find_tags_by_class(browser.contents, 'informational message'))
+    0
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> print_direct_subscribers(browser.contents)
+    Sample Person (Subscribed by Launchpad Janitor)
+    Steve Alexander (Subscribed by Launchpad Janitor)
 
 Subscribers which the current user may unsubscribe (the current user and teams
-they are a member of) display first in the list, before all other subscriptions.
-
-  >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
-  >>> browser.getControl('Person').value = 'testing-spanish-team'
-  >>> browser.getControl('Subscribe user').click()
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> print_direct_subscribers(browser.contents)
-  testing Spanish team (Subscribed by Foo Bar)
-                       (Unsubscribe testing Spanish team)
-  Sample Person (Subscribed by Launchpad Janitor)
-  Steve Alexander (Subscribed by Launchpad Janitor)
-
-== Subscriptions and Duplicate Bugs ==
+they are a member of) display first in the list, before all other
+subscriptions.
+
+    >>> browser.open(bug_1 + '+addsubscriber')
+    >>> browser.getControl('Person').value = 'testing-spanish-team'
+    >>> browser.getControl('Subscribe user').click()
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> print_direct_subscribers(browser.contents)
+    testing Spanish team (Subscribed by Foo Bar)
+                        (Unsubscribe testing Spanish team)
+    Sample Person (Subscribed by Launchpad Janitor)
+    Steve Alexander (Subscribed by Launchpad Janitor)
+
+Subscriptions and Duplicate Bugs
+--------------------------------
 
 Because we auto-subscribe users that are directly subscribed to dupes of
 a bug, we give the option to unsubscribe from dupe target bugs. Behind
@@ -183,13 +182,12 @@
 
     >>> stevea_browser = setupBrowser(
     ...   auth="Basic steve.alexander@xxxxxxxxxxxxxxx:test")
-    >>> stevea_browser.open(
-    ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
+    >>> bug_3 = 'http://launchpad.dev/bugs/3/'
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
     >>> print_subscribers_from_duplicates(stevea_browser.contents)
     From duplicates:
 
-    >>> stevea_browser.open(
-    ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
     >>> print_also_notified(stevea_browser.contents)
     Also notified:
 
@@ -197,98 +195,91 @@
 a dupe of bug #3, then Steve gets indirectly subscribed to bug #3, and
 is presented with the Unsubscribe link instead.
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/tomcat/+bug/2/+duplicate";)
-
-  >>> stevea_browser.getControl("Duplicate Of").value = "3"
-  >>> stevea_browser.getControl("Change").click()
-
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
-  >>> print_subscribers_from_duplicates(stevea_browser.contents)
-  From duplicates:
-  Steve Alexander (Subscribed to bug 2 by Launchpad Janitor)
-                  (Unsubscribe Steve Alexander)
-
-  >>> stevea_browser.getLink(id='unsubscribe-subscriber-11').mech_link.url
-  '+subscribe'
+    >>> bug_2 = 'http://launchpad.dev/tomcat/+bug/2/'
+    >>> stevea_browser.open(bug_2 + "+duplicate")
+
+    >>> stevea_browser.getControl("Duplicate Of").value = "3"
+    >>> stevea_browser.getControl("Change").click()
+
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
+    >>> print_subscribers_from_duplicates(stevea_browser.contents)
+    From duplicates:
+    Steve Alexander (Subscribed to bug 2 by Launchpad Janitor)
+                    (Unsubscribe Steve Alexander)
+
+    >>> stevea_browser.getLink(id='unsubscribe-subscriber-11').mech_link.url
+    '+subscribe'
 
 When he chooses to unsubscribe, he will be unsubscribed from bug #2, the
 dupe of bug #3, so he'll no longer get mail from bug #3.
 
-  >>> stevea_browser.open("http://launchpad.dev/bugs/3";)
-  >>> stevea_browser.getLink('Unsubscribe').click()
-  >>> stevea_browser.getControl("Continue").click()
+    >>> stevea_browser.open("http://launchpad.dev/bugs/3";)
+    >>> stevea_browser.getLink('Unsubscribe').click()
+    >>> stevea_browser.getControl("Continue").click()
 
 # XXX: Brad Bollenbach 2006-09-27 bug=62634: Printing the tag here,
 # instead of tag.string.
 
-  >>> for tag in find_tags_by_class(
-  ...     stevea_browser.contents, 'informational message'):
-  ...   print tag.renderContents()
-  You have been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
+    >>> for tag in find_tags_by_class(
+    ...     stevea_browser.contents, 'informational message'):
+    ...   print tag.renderContents()
+    You have been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
 
 (Except for Mark, who has a structural subscription to the target,
 there are no longer any indirect subscribers, because Steve was
 unsubscribed from the dupes and thus is no longer indirectly subscribed
 to bug #3.)
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
-  >>> print_subscribers_from_duplicates(stevea_browser.contents)
-  From duplicates:
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
+    >>> print_subscribers_from_duplicates(stevea_browser.contents)
+    From duplicates:
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
-  >>> print_also_notified(stevea_browser.contents)
-  Also notified:
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
+    >>> print_also_notified(stevea_browser.contents)
+    Also notified:
 
 Let's repeat this example, with Steve subscribed to two different dupes,
 to see how the unsubscribe notification changes slightly, because he
 gets unsubscribed from more than one duplicate.
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/firefox/+bug/1/+duplicate";)
-  >>> stevea_browser.getControl("Duplicate Of").value = "3"
-  >>> stevea_browser.getControl("Change").click()
+    >>> stevea_browser.open(bug_1 + "+duplicate")
+    >>> stevea_browser.getControl("Duplicate Of").value = "3"
+    >>> stevea_browser.getControl("Change").click()
 
 (Resubscribe Steve to bug #2, because he was unsubscribed in the
 previous example.)
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/tomcat/+bug/2/+addsubscriber";)
-  >>> stevea_browser.getControl('Person').value = 'stevea'
-  >>> stevea_browser.getControl('Subscribe user').click()
-
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
-  >>> print_subscribers_from_duplicates(stevea_browser.contents)
-  From duplicates:
-  Sample Person (Subscribed ...)
-  Steve Alexander (Subscribed ...) (Unsubscribe Steve Alexander)
-  testing Spanish team (Subscribed to bug 1 by Foo Bar)
-
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
-  >>> print_also_notified(stevea_browser.contents)
-  Also notified:
-
-  >>> stevea_browser.open("http://launchpad.dev/bugs/3";)
-  >>> stevea_browser.getLink("Unsubscribe").click()
-  >>> stevea_browser.getControl("Continue").click()
-
-  >>> for tag in find_tags_by_class(
-  ...     stevea_browser.contents, 'informational message'):
-  ...   print tag.renderContents()
-  You have been unsubscribed from bug 3 and 2 duplicates (<a...#1</a>, <a...#2</a>)...
+    >>> stevea_browser.open(bug_2 + "+addsubscriber")
+    >>> stevea_browser.getControl('Person').value = 'stevea'
+    >>> stevea_browser.getControl('Subscribe user').click()
+
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
+    >>> print_subscribers_from_duplicates(stevea_browser.contents)
+    From duplicates:
+    Sample Person (Subscribed ...)
+    Steve Alexander (Subscribed ...) (Unsubscribe Steve Alexander)
+    testing Spanish team (Subscribed to bug 1 by Foo Bar)
+
+    >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
+    >>> print_also_notified(stevea_browser.contents)
+    Also notified:
+
+    >>> stevea_browser.open("http://launchpad.dev/bugs/3";)
+    >>> stevea_browser.getLink("Unsubscribe").click()
+    >>> stevea_browser.getControl("Continue").click()
+
+    >>> for tag in find_tags_by_class(
+    ...     stevea_browser.contents, 'informational message'):
+    ...   print tag.renderContents()
+    You have been unsubscribed from bug 3 and 2 duplicates (<a...#1</a>, <a...#2</a>)...
 
 (Let's undupe bug #1 from bug #3, since it's unneeded for the examples
 that follow.)
 
-  >>> stevea_browser.open(
-  ...     "http://launchpad.dev/firefox/+bug/1/+duplicate";)
-  >>> stevea_browser.getControl("Duplicate Of").value = ""
-  >>> stevea_browser.getControl("Change").click()
+    >>> stevea_browser.open(
+    ...     "http://launchpad.dev/firefox/+bug/1/+duplicate";)
+    >>> stevea_browser.getControl("Duplicate Of").value = ""
+    >>> stevea_browser.getControl("Change").click()
 
 This unsubscribe behaviour is team-aware too, so you can unsubscribe
 your teams from a bug, even when the team's subscription comes from a
@@ -296,76 +287,76 @@
 bug #2, and notice how bug #3's indirect subscriptions are update to
 include that team.
 
-  >>> foobar_browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
-  >>> foobar_browser.open("http://launchpad.dev/bugs/2";)
-  >>> foobar_browser.getLink('Subscribe someone else').click()
-  >>> foobar_browser.getControl("Person").value = "ubuntu-team"
-  >>> foobar_browser.getControl("Subscribe user").click()
-
-  >>> foobar_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
-
-  >>> print_subscribers_from_duplicates(foobar_browser.contents)
-  From duplicates:
-  Ubuntu Team (Subscribed to bug 2 by Foo Bar) (Unsubscribe Ubuntu Team)
-
-  >>> foobar_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
-
-  >>> print_also_notified(foobar_browser.contents)
-  Also notified:
+    >>> foobar_browser = setupBrowser(auth="Basic foo.bar@xxxxxxxxxxxxx:test")
+    >>> foobar_browser.open("http://launchpad.dev/bugs/2";)
+    >>> foobar_browser.getLink('Subscribe someone else').click()
+    >>> foobar_browser.getControl("Person").value = "ubuntu-team"
+    >>> foobar_browser.getControl("Subscribe user").click()
+
+    >>> foobar_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
+
+    >>> print_subscribers_from_duplicates(foobar_browser.contents)
+    From duplicates:
+    Ubuntu Team (Subscribed to bug 2 by Foo Bar) (Unsubscribe Ubuntu Team)
+
+    >>> foobar_browser.open(
+    ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
+
+    >>> print_also_notified(foobar_browser.contents)
+    Also notified:
 
 The subscribe link for Foo Bar still says "Subscribe", because
 Foo Bar can subscribe himself directly to this bug. For unsubscribing
 the team, the (-) icon can be used. In reality, the two links point to
 the same page, but that is changed when the page is AJAX enabled.
 
-  >>> foobar_browser.open("http://launchpad.dev/bugs/3";)
-  >>> foobar_browser.getLink('Subscribe').click()
+    >>> foobar_browser.open("http://launchpad.dev/bugs/3";)
+    >>> foobar_browser.getLink('Subscribe').click()
 
 Foo Bar can unsubscribe ubuntu-team, and ubuntu-team will no longer show
 up in the indirect subscriptions.
 
-  >>> subscription_field = foobar_browser.getControl(name="field.subscription")
-  >>> subscription_field.value = ["ubuntu-team"]
-  >>> foobar_browser.getControl("Continue").click()
+    >>> subscription_field = foobar_browser.getControl(
+    ...     name="field.subscription")
+    >>> subscription_field.value = ["ubuntu-team"]
+    >>> foobar_browser.getControl("Continue").click()
 
-  >>> for tag in find_tags_by_class(
-  ...     foobar_browser.contents, 'informational message'):
-  ...   print tag.renderContents()
-  Ubuntu Team has been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
+    >>> for tag in find_tags_by_class(
+    ...     foobar_browser.contents, 'informational message'):
+    ...   print tag.renderContents()
+    Ubuntu Team has been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
 
 (ubuntu-team is no longer an indirect subscriber.)
 
-  >>> foobar_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
-  >>> print_subscribers_from_duplicates(foobar_browser.contents)
-  From duplicates:
-
-  >>> foobar_browser.open(
-  ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
-  >>> print_also_notified(foobar_browser.contents)
-  Also notified:
-
-
-== Displaying subscribers ==
+    >>> foobar_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
+    >>> print_subscribers_from_duplicates(foobar_browser.contents)
+    From duplicates:
+
+    >>> foobar_browser.open(
+    ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
+    >>> print_also_notified(foobar_browser.contents)
+    Also notified:
+
+
+Displaying subscribers
+----------------------
 
 The display names of subscribers are escaped in the subscribers list, they are
-also trimmed to 20 characters, so that they fit alongside the unsubscribe icon.
+also trimmed to 20 characters, so that they fit alongside the unsubscribe
+icon.
 
-  >>> login(ANONYMOUS)
-  >>> abuser = factory.makePerson(
-  ...     name='abuser',
-  ...     displayname='<script>javascript:alert("YO")</script>')
-  >>> logout()
-  >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
-  >>> browser.getControl('Person').value = 'abuser'
-  >>> browser.getControl('Subscribe user').click()
-  >>> browser.open(
-  ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
-  >>> subscriber_list = find_tag_by_id(
-  ...     browser.contents, 'subscribers-direct')
-  >>> for subscriber in subscriber_list.findAll('div'):
-  ...     if '~abuser' in subscriber.a['href']:
-  ...         print subscriber.a.contents[2].strip()
-  &lt;script&gt;javascrip...
+    >>> login(ANONYMOUS)
+    >>> abuser = factory.makePerson(
+    ...     name='abuser',
+    ...     displayname='<script>javascript:alert("YO")</script>')
+    >>> logout()
+    >>> browser.open(bug_1 + '+addsubscriber')
+    >>> browser.getControl('Person').value = 'abuser'
+    >>> browser.getControl('Subscribe user').click()
+    >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
+    >>> subscriber_list = find_tag_by_id(
+    ...     browser.contents, 'subscribers-direct')
+    >>> for subscriber in subscriber_list.findAll('div'):
+    ...     if '~abuser' in subscriber.a['href']:
+    ...         print subscriber.a.contents[2].strip()
+    &lt;script&gt;javascrip...

=== modified file 'lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt'
--- lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt	2011-03-10 17:03:32 +0000
+++ lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt	2011-05-24 16:07:40 +0000
@@ -1,4 +1,5 @@
-= Bug statistics portlet =
+Bug statistics portlet
+======================
 
 The distribution, project group and project bug listings contain a
 portlet that shows bug statistics for the target. Each statistic is a
@@ -6,7 +7,8 @@
 served in a separate request; the request is issued via Javascript and
 inserted into the page later.
 
-== Distribution ==
+Distribution
+------------
 
     >>> path = 'debian'
 
@@ -77,7 +79,8 @@
     http://bugs.launchpad.dev/debian/+cve
 
 
-== Distribution Series ==
+Distribution Series
+-------------------
 
     >>> path = 'debian/woody'
 
@@ -149,7 +152,8 @@
     http://bugs.launchpad.dev/debian/woody/+cve
 
 
-== Distribution Source Package ==
+Distribution Source Package
+---------------------------
 
     >>> path = 'debian/+source/mozilla-firefox'
 
@@ -219,7 +223,8 @@
     LinkNotFoundError
 
 
-== Source Package in Distribution Series ==
+Source Package in Distribution Series
+-------------------------------------
 
     >>> path = 'debian/woody/+source/mozilla-firefox'
 
@@ -286,7 +291,8 @@
     LinkNotFoundError
 
 
-== Project group ==
+Project group
+-------------
 
     >>> path = 'mozilla'
 
@@ -356,7 +362,8 @@
     LinkNotFoundError
 
 
-== Project ==
+Project
+-------
 
     >>> path = 'firefox'