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

Proposed by Benji York
Status: Rejected
Rejected by: Benji York
Proposed branch: lp:~benji/launchpad/bug-784575-message
Merge into: lp:launchpad
Diff against target: 1073 lines (+344/-362)
7 files modified
lib/lp/bugs/doc/bugnotification-sending.txt (+17/-80)
lib/lp/bugs/emailtemplates/bug-notification-verbose.txt (+1/-1)
lib/lp/bugs/emailtemplates/bug-notification.txt (+1/-1)
lib/lp/bugs/scripts/bugnotification.py (+12/-10)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+52/-12)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt (+247/-251)
lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt (+14/-7)
To merge this branch: bzr merge lp:~benji/launchpad/bug-784575-message
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) code Approve
Review via email: mp+62157@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (5.7 KiB)

Love this branch, love the cover letter. An MP for the textbooks: good approach with the preparatory cleanups, MP explaining all the right things. And last but not least it's great to see pointlessly brittle doctest checks disappear.

There are still a few nits to pick:

=== 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:59:39 +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)

While you're here, could you also fix the line break on that last string, and use double quotes so we don't have panic and kitchen fires when an apostrophe shows up?

=== 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:59:39 +0000

@@ -1145,7 +1149,55 @@
         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.

Is this bug worth a whole test case though? Surely the test would fit comfortably into some other test case?

+ 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.INCOMPLE...

Read more...

review: Approve (code)
Revision history for this message
Benji York (benji) wrote :
Download full text (6.8 KiB)

On Wed, May 25, 2011 at 1:09 AM, Jeroen T. Vermeulen <email address hidden> wrote:
> === 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:59:39 +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)
>
> While you're here, could you also fix the line break on that last
> string,

I'm afraid I don't follow. Do you mean like so?

            subscriptions_message = (
                'To manage notifications about this bug go to:\n%s'
                % notification_url)

That does read better, so I'll change it to that, but if you meant
something else, I'm curious to know what.

> and use double quotes so we don't have panic and kitchen fires
> when an apostrophe shows up?

I'm a double-quoted-string hater, but you buttered me up so well with
all the "MP for the textbooks" stuff that I'll cave. ;)

> === 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:59:39 +0000
>
> @@ -1145,7 +1149,55 @@
>         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.
>
> Is this bug worth a whole test case though?  Surely the test would fit
> comfortably into some other test case?

I couldn't find a good spot. Most of the tests for this code are
doctests and since we have a a moratorium on new doctests I figured
adding a point test case was the best option. I'll look again to see if
I can find a good spot... Nope...

Read more...

Revision history for this message
Benji York (benji) wrote :

On Wed, May 25, 2011 at 11:09 AM, Benji York <email address hidden> wrote:
> On Wed, May 25, 2011 at 1:09 AM, Jeroen T. Vermeulen <email address hidden> wrote:
>> +        (message,) = construct_email_notifications([notification])[2]
>>
>> The "[2]" is very obscure.  That's why we generally prefer to drill
>> into tuples by unpacking them:
>>
>>    filtered_notifs, omitted_notifs, messages = construct_email_notifications(
>>        [notification])
>>    (message, ) = messages
>
> My preferences fall the other way (i.e., not minding the indexing and
> disliking the unused names), but I'll go with your request.
>
> I did combine the two expressions into one though:
>
>    filtered, omitted, (message,) = construct_email_notifications(
>        [notification])

Nope, that won't work. The linter shares my dislike for unused names.
I ended up with this instead:

    _, _, (message,) = construct_email_notifications([notification])
--
Benji York

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> On Wed, May 25, 2011 at 1:09 AM, Jeroen T. Vermeulen <email address hidden>
> wrote:

> > While you're here, could you also fix the line break on that last
> > string,
>
> I'm afraid I don't follow. Do you mean like so?
>
>            subscriptions_message = (
> 'To manage notifications about this bug go to:\n%s'
> % notification_url)
>
> That does read better, so I'll change it to that, but if you meant
> something else, I'm curious to know what.

You did follow after all then. :)

> > and use double quotes so we don't have panic and kitchen fires
> > when an apostrophe shows up?
>
> I'm a double-quoted-string hater, but you buttered me up so well with
> all the "MP for the textbooks" stuff that I'll cave. ;)

*Evil chuckle*

> > === 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:59:39
> +0000

> > Is this bug worth a whole test case though?  Surely the test would fit
> > comfortably into some other test case?
>
> I couldn't find a good spot. Most of the tests for this code are
> doctests and since we have a a moratorium on new doctests I figured
> adding a point test case was the best option. I'll look again to see if
> I can find a good spot... Nope, this looks like the best thing to do.

No worries then.

> > +        (message,) = construct_email_notifications([notification])[2]
> >
> > The "[2]" is very obscure.  That's why we generally prefer to drill
> > into tuples by unpacking them:
> >
> >    filtered_notifs, omitted_notifs, messages =
> construct_email_notifications(
> >        [notification])
> >    (message, ) = messages
>
> My preferences fall the other way (i.e., not minding the indexing and
> disliking the unused names), but I'll go with your request.

…but it turned out that "make lint" didn't like that. Annoying, that: the unpacking was once a solid rule. Using "_" as the variable name is one way out, but doesn't combine very well with the existence of gettext. This is where I throw my hands up and stop offering clever advice.

> I did combine the two expressions into one though:
>
> filtered, omitted, (message,) = construct_email_notifications(
> [notification])

Didn't know python was smart enough to match that, and never dared try!

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
2--- lib/lp/bugs/doc/bugnotification-sending.txt 2011-05-18 13:00:11 +0000
3+++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-05-25 16:14:50 +0000
4@@ -76,12 +76,6 @@
5 a comment.
6 <BLANKLINE>
7 ...
8- You received this bug notification because you are subscribed to
9- mozilla-firefox in Ubuntu.
10- http://bugs.launchpad.dev/bugs/1
11- <BLANKLINE>
12- Title:
13- Firefox does not support SVG
14 ----------------------------------------------------------------------
15 To: mark@example.com
16 From: Sample Person <1@bugs.launchpad.net>
17@@ -91,11 +85,6 @@
18 a comment.
19 <BLANKLINE>
20 ...
21- You received this bug notification because you are a bug assignee.
22- http://bugs.launchpad.dev/bugs/1
23- <BLANKLINE>
24- Title:
25- Firefox does not support SVG
26 ----------------------------------------------------------------------
27 To: support@ubuntu.com
28 From: Sample Person <1@bugs.launchpad.net>
29@@ -104,13 +93,7 @@
30 <BLANKLINE>
31 a comment.
32 <BLANKLINE>
33- --
34- You received this bug notification because you are a member of Ubuntu
35- Team, which is the registrant for Ubuntu.
36- http://bugs.launchpad.dev/bugs/1
37- <BLANKLINE>
38- Title:
39- Firefox does not support SVG
40+ ...
41 ----------------------------------------------------------------------
42 To: test@canonical.com
43 From: Sample Person <1@bugs.launchpad.net>
44@@ -120,14 +103,6 @@
45 a comment.
46 <BLANKLINE>
47 ...
48- You received this bug notification because you are a direct subscriber
49- of the bug.
50- http://bugs.launchpad.dev/bugs/1
51- <BLANKLINE>
52- Title:
53- Firefox does not support SVG
54- <BLANKLINE>
55- ...
56 ----------------------------------------------------------------------
57
58 You can see that the message above contains the bug's initial comment's
59@@ -192,13 +167,7 @@
60 <BLANKLINE>
61 a new comment.
62 <BLANKLINE>
63- --
64- You received this bug notification because you are a member of Ubuntu
65- Team, which is the registrant for Ubuntu.
66- http://bugs.launchpad.dev/bugs/1
67- <BLANKLINE>
68- Title:
69- Firefox does not support SVG
70+ ...
71 ----------------------------------------------------------------------
72 To: test@canonical.com
73 ...
74@@ -242,13 +211,8 @@
75 <BLANKLINE>
76 ** Visibility changed to: Private
77 <BLANKLINE>
78+ --
79 ...
80- You received this bug notification because you are a member of Ubuntu
81- Team, which is the registrant for Ubuntu.
82- http://bugs.launchpad.dev/bugs/1
83- <BLANKLINE>
84- Title:
85- Firefox does not support SVG
86 ----------------------------------------------------------------------
87 To: test@canonical.com
88 ...
89@@ -300,12 +264,7 @@
90 + Another summary
91 <BLANKLINE>
92 --
93- You received this bug notification because you are a member of Ubuntu
94- Team, which is the registrant for Ubuntu.
95- http://bugs.launchpad.dev/bugs/1
96- <BLANKLINE>
97- Title:
98- Firefox does not support SVG
99+ ...
100 ----------------------------------------------------------------------
101 To: test@canonical.com
102 ...
103@@ -445,16 +404,7 @@
104 <BLANKLINE>
105 *** This bug is a duplicate of bug 1 ***
106 http://bugs.launchpad.dev/bugs/1
107- <BLANKLINE>
108- a comment.
109- <BLANKLINE>
110- --
111- You received this bug notification because you are a member of Ubuntu
112- Team, which is the registrant for Ubuntu.
113- http://bugs.launchpad.dev/bugs/16
114- <BLANKLINE>
115- Title:
116- new bug
117+ ...
118 ----------------------------------------------------------------------
119 To: test@canonical.com
120 From: Sample Person <16@bugs.launchpad.net>
121@@ -463,19 +413,7 @@
122 <BLANKLINE>
123 *** This bug is a duplicate of bug 1 ***
124 http://bugs.launchpad.dev/bugs/1
125- <BLANKLINE>
126- a comment.
127- <BLANKLINE>
128- --
129- You received this bug notification because you are a direct subscriber
130- of the bug.
131- http://bugs.launchpad.dev/bugs/16
132- <BLANKLINE>
133- Title:
134- new bug
135- <BLANKLINE>
136- To unsubscribe from this bug, go to:
137- http://bugs.launchpad.dev/ubuntu/+bug/16/+subscribe
138+ ...
139 ----------------------------------------------------------------------
140
141 >>> flush_notifications()
142@@ -585,7 +523,8 @@
143 ... ten_minutes_ago, sample_person, "title",
144 ... True, False))
145
146- >>> notifications = getUtility(IBugNotificationSet).getNotificationsToSend()
147+ >>> notifications = getUtility(
148+ ... IBugNotificationSet).getNotificationsToSend()
149 >>> len(notifications)
150 8
151
152@@ -660,12 +599,7 @@
153 <BLANKLINE>
154 -- =
155 <BLANKLINE>
156- You received this bug notification because you are subscribed to
157- mozilla-firefox in Ubuntu.
158- http://bugs.launchpad.dev/bugs/1
159- <BLANKLINE>
160- Title:
161- Firefox does not support SVG
162+ You received this bug notification because...
163 INFO Notifying mark@example.com about bug 1.
164 ...
165 INFO Notifying owner@example.com about bug 1.
166@@ -1051,7 +985,7 @@
167 >>> print_notification(collated_messages['concise@example.com'][0])
168 To: concise@example.com
169 ...
170- To unsubscribe from this bug, go to:...
171+ To manage notifications about this bug go to:...
172
173 Verbose Team Person gets a concise email, even though they belong to a team
174 that gets verbose email.
175@@ -1099,8 +1033,8 @@
176 will be automatically wrapped by the BugNotification
177 machinery. Ain't technology great?
178 <BLANKLINE>
179- To unsubscribe from this bug, go to:
180- http://bugs.launchpad.dev/.../+bug/.../+subscribe
181+ To manage notifications about this bug go to:
182+ http://bugs.launchpad.dev/.../+bug/.../+subscriptions
183 ----------------------------------------------------------------------
184
185 And Concise Team Person does too, even though his team doesn't want them:
186@@ -1128,6 +1062,9 @@
187 This is a long description of the bug, which
188 will be automatically wrapped by the BugNotification
189 machinery. Ain't technology great?
190+ <BLANKLINE>
191+ To manage notifications about this bug go to:
192+ http://bugs.launchpad.dev/.../+bug/.../+subscriptions
193 ----------------------------------------------------------------------
194
195 It's important to note that the bug title and description are wrapped
196@@ -1143,7 +1080,7 @@
197 ...
198 'Bug description:',
199 ' This is a long description of the bug, which will be automatically',
200- " wrapped by the BugNotification machinery. Ain't technology great?"]
201+ " wrapped by the BugNotification machinery. Ain't technology great?"...]
202
203 The title is also wrapped and indented in normal notifications.
204
205@@ -1153,7 +1090,7 @@
206 [...
207 'Title:',
208 ' In the beginning, the universe was created. This has made a lot of',
209- ' people very angry and has been widely regarded as a bad move']
210+ ' people very angry and has been widely regarded as a bad move'...]
211
212 Self-Generated Bug Notifications
213 --------------------------------
214
215=== modified file 'lib/lp/bugs/emailtemplates/bug-notification-verbose.txt'
216--- lib/lp/bugs/emailtemplates/bug-notification-verbose.txt 2011-03-02 16:22:36 +0000
217+++ lib/lp/bugs/emailtemplates/bug-notification-verbose.txt 2011-05-25 16:14:50 +0000
218@@ -12,4 +12,4 @@
219 Bug description:
220 %(bug_description)s
221
222-%(unsubscribe_notice)s
223+%(subscriptions_message)s
224
225=== modified file 'lib/lp/bugs/emailtemplates/bug-notification.txt'
226--- lib/lp/bugs/emailtemplates/bug-notification.txt 2011-03-02 16:22:36 +0000
227+++ lib/lp/bugs/emailtemplates/bug-notification.txt 2011-05-25 16:14:50 +0000
228@@ -7,4 +7,4 @@
229 Title:
230 %(bug_title)s
231
232-%(unsubscribe_notice)s
233+%(subscriptions_message)s
234
235=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
236--- lib/lp/bugs/scripts/bugnotification.py 2011-04-05 22:34:35 +0000
237+++ lib/lp/bugs/scripts/bugnotification.py 2011-05-25 16:14:50 +0000
238@@ -193,25 +193,27 @@
239 data['filter descriptions'])
240 else:
241 filters_text = u""
242- # XXX deryck 2009-11-17 Bug #484319
243- # This should be refactored to add a link inside the
244- # code where we build `reason`. However, this will
245- # require some extra work, and this small change now
246- # will ease pain for a lot of unhappy users.
247- if 'direct subscriber' in reason and 'member of' not in reason:
248- unsubscribe_notice = ('To unsubscribe from this bug, go to:\n'
249- '%s/+subscribe' % canonical_url(bug.bugtasks[0]))
250+
251+ # In the rare case of a bug with no bugtasks, we can't generate the
252+ # subscription management URL so just leave off the subscription
253+ # management message entirely.
254+ if len(bug.bugtasks):
255+ bug_url = canonical_url(bug.bugtasks[0])
256+ notification_url = bug_url + '/+subscriptions'
257+ subscriptions_message = (
258+ "To manage notifications about this bug go to:\n%s"
259+ % notification_url)
260 else:
261- unsubscribe_notice = ''
262+ subscriptions_message = ''
263
264 data_wrapper = MailWrapper(width=72, indent=' ')
265 body_data = {
266 'content': mail_wrapper.format(content),
267 'bug_title': data_wrapper.format(bug.title),
268 'bug_url': canonical_url(bug),
269- 'unsubscribe_notice': unsubscribe_notice,
270 'notification_rationale': mail_wrapper.format(reason),
271 'subscription_filters': filters_text,
272+ 'subscriptions_message': subscriptions_message,
273 }
274
275 # If the person we're sending to receives verbose notifications
276
277=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
278--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-05-12 21:33:10 +0000
279+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-05-25 16:14:50 +0000
280@@ -5,6 +5,7 @@
281 __metaclass__ = type
282
283 from datetime import datetime, timedelta
284+import re
285 import unittest
286
287 import pytz
288@@ -20,7 +21,10 @@
289 sqlvalues,
290 )
291 from canonical.launchpad.ftests import login
292-from canonical.launchpad.helpers import get_contact_email_addresses
293+from canonical.launchpad.helpers import (
294+ get_contact_email_addresses,
295+ get_email_template,
296+ )
297 from canonical.launchpad.interfaces.lpstorm import IStore
298 from lp.services.messages.interfaces.message import IMessageSet
299 from canonical.testing.layers import LaunchpadZopelessLayer
300@@ -929,7 +933,7 @@
301
302 def setUp(self):
303 super(TestEmailNotificationsWithFilters, self).setUp()
304- self.bug=self.factory.makeBug()
305+ self.bug = self.factory.makeBug()
306 subscriber = self.factory.makePerson()
307 self.subscription = self.bug.default_bugtask.target.addSubscription(
308 subscriber, subscriber)
309@@ -1104,6 +1108,14 @@
310 self.getSubscriptionEmailHeaders())
311
312
313+def fetch_notifications(subscriber, bug):
314+ return IStore(BugNotification).find(
315+ BugNotification,
316+ BugNotification.id == BugNotificationRecipient.bug_notificationID,
317+ BugNotificationRecipient.personID == subscriber.id,
318+ BugNotification.bug == bug)
319+
320+
321 class TestEmailNotificationsWithFiltersWhenBugCreated(TestCaseWithFactory):
322 # See bug 720147.
323
324@@ -1128,11 +1140,7 @@
325 comment=message, owner=self.submitter,
326 status=BugTaskStatus.NEW)
327 bug = self.product.createBug(params)
328- notification = IStore(BugNotification).find(
329- BugNotification,
330- BugNotification.id==BugNotificationRecipient.bug_notificationID,
331- BugNotificationRecipient.personID == self.subscriber.id,
332- BugNotification.bug == bug).one()
333+ notification = fetch_notifications(self.subscriber, bug).one()
334 self.assertEqual(notification.message.text_contents, message)
335
336 def test_filters_do_not_match_when_bug_is_created(self):
337@@ -1143,9 +1151,41 @@
338 status=BugTaskStatus.TRIAGED,
339 importance=BugTaskImportance.HIGH)
340 bug = self.product.createBug(params)
341- notifications = IStore(BugNotification).find(
342- BugNotification,
343- BugNotification.id==BugNotificationRecipient.bug_notificationID,
344- BugNotificationRecipient.personID == self.subscriber.id,
345- BugNotification.bug == bug)
346+ notifications = fetch_notifications(self.subscriber, bug)
347 self.assertTrue(notifications.is_empty())
348+
349+
350+class TestManageNotificationsMessage(TestCaseWithFactory):
351+
352+ layer = LaunchpadZopelessLayer
353+
354+ def test_manage_notifications_message_is_included(self):
355+ # Set up a subscription to a product.
356+ subscriber = self.factory.makePerson()
357+ submitter = self.factory.makePerson()
358+ product = self.factory.makeProduct(
359+ bug_supervisor=submitter)
360+ product.addSubscription(subscriber, subscriber)
361+ # Create a bug that will match the subscription.
362+ bug = product.createBug(CreateBugParams(
363+ title=self.factory.getUniqueString(),
364+ comment=self.factory.getUniqueString(),
365+ owner=submitter))
366+ notification = fetch_notifications(subscriber, bug).one()
367+ _, _, (message,) = construct_email_notifications([notification])
368+ payload = message.get_payload()
369+ self.assertThat(payload, Contains(
370+ 'To manage notifications about this bug go to:\nhttp://'))
371+
372+
373+class TestNotificationSignatureSeparator(TestCase):
374+
375+ def test_signature_separator(self):
376+ # Email signatures are often separated from the body of a message by a
377+ # special separator so user agents can identify the signature for
378+ # special treatment (hiding, stripping when replying, colorizing,
379+ # etc.). The bug notification messages follow the convention.
380+ names = ['bug-notification-verbose.txt', 'bug-notification.txt']
381+ for name in names:
382+ template = get_email_template(name, 'bugs')
383+ self.assertTrue(re.search('^-- $', template, re.MULTILINE))
384
385=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt'
386--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2010-12-23 12:55:53 +0000
387+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt 2011-05-25 16:14:50 +0000
388@@ -1,178 +1,178 @@
389-= Personal Subscriptions =
390+Personal Subscriptions
391+======================
392
393 Users can subscribe to bugs reported in Launchpad, via the "Subscribe" link
394 in the actions portlet.
395
396- >>> from lp.bugs.tests.bug import (
397- ... print_direct_subscribers, print_also_notified,
398- ... print_subscribers_from_duplicates)
399+ >>> from lp.bugs.tests.bug import (
400+ ... print_direct_subscribers, print_also_notified,
401+ ... print_subscribers_from_duplicates)
402
403- >>> browser = setupBrowser(auth='Basic foo.bar@canonical.com:test')
404- >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
405- >>> browser.url
406- 'http://bugs.launchpad.dev/firefox/+bug/1'
407+ >>> browser = setupBrowser(auth='Basic foo.bar@canonical.com:test')
408+ >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
409+ >>> browser.url
410+ 'http://bugs.launchpad.dev/firefox/+bug/1'
411
412 >>> browser.getLink('Subscribe').click()
413
414- >>> subscription_widget = browser.getControl(name='field.subscription')
415- >>> subscription_widget.options
416- ['name16']
417- >>> subscription_widget.value
418- ['name16']
419+ >>> subscription_widget = browser.getControl(name='field.subscription')
420+ >>> subscription_widget.options
421+ ['name16']
422+ >>> subscription_widget.value
423+ ['name16']
424
425- >>> submit = browser.getControl('Continue')
426+ >>> submit = browser.getControl('Continue')
427
428 Clicking "Continue" subscribes the user to the bug, and tells the user
429 this.
430
431- >>> submit.click()
432-
433- >>> browser.url
434- 'http://bugs.launchpad.dev/firefox/+bug/1'
435-
436- >>> for tag in find_tags_by_class(browser.contents, "informational message"):
437- ... print tag.renderContents()
438- You have been subscribed to this bug.
439+ >>> submit.click()
440+
441+ >>> browser.url
442+ 'http://bugs.launchpad.dev/firefox/+bug/1'
443+
444+ >>> tags = find_tags_by_class(browser.contents, "informational message")
445+ >>> for tag in tags:
446+ ... print tag.renderContents()
447+ You have been subscribed to this bug.
448
449 There's also now a link to unsubscribe the user next to the name. It's a
450 relative URL to +subscribe, so it will only work when the portlet is
451 in the context of the bug page.
452
453- >>> browser.open(
454- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
455- >>> print_direct_subscribers(browser.contents)
456- Foo Bar (Self-subscribed) (Unsubscribe Foo Bar)
457- Sample Person (Subscribed by Launchpad Janitor)
458- Steve Alexander (Subscribed by Launchpad Janitor)
459-
460- >>> browser.open(
461- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
462- >>> link = browser.getLink(id='unsubscribe-subscriber-16')
463- >>> print link.mech_link.url
464- +subscribe
465-
466- >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/')
467- >>> browser.getLink('Unsubscribe').click()
468- >>> print browser.title
469- Bug #1...
470- >>> browser.url
471- 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
472+ >>> bug_1 = 'http://bugs.launchpad.dev/bugs/1/'
473+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
474+ >>> print_direct_subscribers(browser.contents)
475+ Foo Bar (Self-subscribed) (Unsubscribe Foo Bar)
476+ Sample Person (Subscribed by Launchpad Janitor)
477+ Steve Alexander (Subscribed by Launchpad Janitor)
478+
479+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
480+ >>> link = browser.getLink(id='unsubscribe-subscriber-16')
481+ >>> print link.mech_link.url
482+ +subscribe
483+
484+ >>> browser.open(bug_1)
485+ >>> browser.getLink('Unsubscribe').click()
486+ >>> print browser.title
487+ Bug #1...
488+ >>> browser.url
489+ 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
490
491
492 Clicking the "Continue" button from the +subscribe page will unsubscribe
493 the user this time, and inform the user.
494
495- >>> subscription_widget.value
496- ['name16']
497- >>> submit = browser.getControl('Continue')
498- >>> submit.click()
499-
500- >>> browser.url
501- 'http://bugs.launchpad.dev/firefox/+bug/1/'
502-
503- >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
504- ... print tag.renderContents()
505- You have been unsubscribed from bug 1.
506+ >>> subscription_widget.value
507+ ['name16']
508+ >>> submit = browser.getControl('Continue')
509+ >>> submit.click()
510+
511+ >>> browser.url
512+ 'http://bugs.launchpad.dev/firefox/+bug/1'
513+
514+ >>> tags = find_tags_by_class(browser.contents, 'informational message')
515+ >>> for tag in tags:
516+ ... print tag.renderContents()
517+ You have been unsubscribed from bug 1.
518
519 Users can unsubscribe teams to which they belong. Let's demonstrate by
520 first subscribing one of Foo Bar's teams.
521
522- >>> browser.getLink('Subscribe someone else').click()
523- >>> browser.url
524- 'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber'
525+ >>> browser.getLink('Subscribe someone else').click()
526+ >>> browser.url
527+ 'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber'
528
529- >>> browser.getControl('Person').value = 'launchpad'
530- >>> browser.getControl('Subscribe user').click()
531- >>> browser.url
532- 'http://bugs.launchpad.dev/firefox/+bug/1'
533+ >>> browser.getControl('Person').value = 'launchpad'
534+ >>> browser.getControl('Subscribe user').click()
535+ >>> browser.url
536+ 'http://bugs.launchpad.dev/firefox/+bug/1'
537
538 There's an unsubscribe link next to the team name.
539
540- >>> browser.open(
541- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
542- >>> print_direct_subscribers(browser.contents)
543- Launchpad Developers (Subscribed by Foo Bar) (Unsubscribe Launchpad
544- Developers)
545- Sample Person (Subscribed by Launchpad Janitor)
546- Steve Alexander (Subscribed by Launchpad Janitor)
547+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
548+ >>> print_direct_subscribers(browser.contents)
549+ Launchpad Developers (Subscribed by Foo Bar) (Unsubscribe Launchpad
550+ Developers)
551+ Sample Person (Subscribed by Launchpad Janitor)
552+ Steve Alexander (Subscribed by Launchpad Janitor)
553
554 Clicking either the subscribe link (for subscribing the user to the bug)
555 or the unsubscribe link for the team gives us the option of both
556 subscribing Foo Bar, and unsubscribing the Launchpad team.
557
558- >>> browser.open(
559- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
560- >>> browser.getLink(id='unsubscribe-subscriber-57').mech_link.url
561- '+subscribe'
562-
563- >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1')
564- >>> browser.getLink('Subscribe').click()
565- >>> browser.url
566- 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
567-
568- >>> subscription_widget = browser.getControl(name='field.subscription')
569- >>> subscription_widget.options
570- ['name16', 'launchpad']
571+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
572+ >>> browser.getLink(id='unsubscribe-subscriber-57').mech_link.url
573+ '+subscribe'
574+
575+ >>> browser.open(bug_1)
576+ >>> browser.getLink('Subscribe').click()
577+ >>> browser.url
578+ 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
579+
580+ >>> subscription_widget = browser.getControl(name='field.subscription')
581+ >>> subscription_widget.options
582+ ['name16', 'launchpad']
583
584 Let's unsubscribe the Launchpad team.
585
586- >>> subscription_widget.value = ['launchpad']
587- >>> browser.getControl('Continue').click()
588-
589- >>> browser.url
590- 'http://bugs.launchpad.dev/firefox/+bug/1'
591-
592- >>> for tag in find_tags_by_class(browser.contents, 'informational message'):
593- ... print tag.renderContents()
594- Launchpad Developers has been unsubscribed from bug 1.
595-
596- >>> browser.open(
597- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
598- >>> print_direct_subscribers(browser.contents)
599- Sample Person (Subscribed by Launchpad Janitor)
600- Steve Alexander (Subscribed by Launchpad Janitor)
601+ >>> subscription_widget.value = ['launchpad']
602+ >>> browser.getControl('Continue').click()
603+
604+ >>> browser.url
605+ 'http://bugs.launchpad.dev/firefox/+bug/1'
606+
607+ >>> tags = find_tags_by_class(browser.contents, 'informational message')
608+ >>> for tag in tags:
609+ ... print tag.renderContents()
610+ Launchpad Developers has been unsubscribed from bug 1.
611+
612+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
613+ >>> print_direct_subscribers(browser.contents)
614+ Sample Person (Subscribed by Launchpad Janitor)
615+ Steve Alexander (Subscribed by Launchpad Janitor)
616
617 On the subscribe page there's a Cancel link as well, that will return
618 the browser to the bug page.
619
620- >>> browser.open(
621- ... 'http://bugs.launchpad.dev/firefox/+bug/1/')
622- >>> browser.getLink('Subscribe').click()
623- >>> browser.url
624- 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
625+ >>> browser.open(bug_1)
626+ >>> browser.getLink('Subscribe').click()
627+ >>> browser.url
628+ 'http://bugs.launchpad.dev/firefox/+bug/1/+subscribe'
629
630- >>> subscription_widget = browser.getControl(name='field.subscription')
631- >>> subscription_widget.value
632- ['name16']
633- >>> browser.getLink('Cancel').click()
634- >>> browser.url
635- 'http://bugs.launchpad.dev/firefox/+bug/1/'
636+ >>> subscription_widget = browser.getControl(name='field.subscription')
637+ >>> subscription_widget.value
638+ ['name16']
639+ >>> browser.getLink('Cancel').click()
640+ >>> browser.url
641+ 'http://bugs.launchpad.dev/firefox/+bug/1'
642
643 Foo Bar wasn't subscribed to the bug.
644
645- >>> len(find_tags_by_class(browser.contents, 'informational message'))
646- 0
647- >>> browser.open(
648- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
649- >>> print_direct_subscribers(browser.contents)
650- Sample Person (Subscribed by Launchpad Janitor)
651- Steve Alexander (Subscribed by Launchpad Janitor)
652+ >>> len(find_tags_by_class(browser.contents, 'informational message'))
653+ 0
654+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
655+ >>> print_direct_subscribers(browser.contents)
656+ Sample Person (Subscribed by Launchpad Janitor)
657+ Steve Alexander (Subscribed by Launchpad Janitor)
658
659 Subscribers which the current user may unsubscribe (the current user and teams
660-they are a member of) display first in the list, before all other subscriptions.
661-
662- >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
663- >>> browser.getControl('Person').value = 'testing-spanish-team'
664- >>> browser.getControl('Subscribe user').click()
665- >>> browser.open(
666- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
667- >>> print_direct_subscribers(browser.contents)
668- testing Spanish team (Subscribed by Foo Bar)
669- (Unsubscribe testing Spanish team)
670- Sample Person (Subscribed by Launchpad Janitor)
671- Steve Alexander (Subscribed by Launchpad Janitor)
672-
673-== Subscriptions and Duplicate Bugs ==
674+they are a member of) display first in the list, before all other
675+subscriptions.
676+
677+ >>> browser.open(
678+ ... 'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
679+ >>> browser.getControl('Person').value = 'testing-spanish-team'
680+ >>> browser.getControl('Subscribe user').click()
681+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
682+ >>> print_direct_subscribers(browser.contents)
683+ testing Spanish team (Subscribed by Foo Bar)
684+ (Unsubscribe testing Spanish team)
685+ Sample Person (Subscribed by Launchpad Janitor)
686+ Steve Alexander (Subscribed by Launchpad Janitor)
687+
688+Subscriptions and Duplicate Bugs
689+--------------------------------
690
691 Because we auto-subscribe users that are directly subscribed to dupes of
692 a bug, we give the option to unsubscribe from dupe target bugs. Behind
693@@ -183,13 +183,12 @@
694
695 >>> stevea_browser = setupBrowser(
696 ... auth="Basic steve.alexander@ubuntulinux.com:test")
697- >>> stevea_browser.open(
698- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
699+ >>> bug_3 = 'http://launchpad.dev/bugs/3/'
700+ >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
701 >>> print_subscribers_from_duplicates(stevea_browser.contents)
702 From duplicates:
703
704- >>> stevea_browser.open(
705- ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
706+ >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
707 >>> print_also_notified(stevea_browser.contents)
708 Also notified:
709
710@@ -197,98 +196,92 @@
711 a dupe of bug #3, then Steve gets indirectly subscribed to bug #3, and
712 is presented with the Unsubscribe link instead.
713
714- >>> stevea_browser.open(
715- ... "http://launchpad.dev/tomcat/+bug/2/+duplicate")
716-
717- >>> stevea_browser.getControl("Duplicate Of").value = "3"
718- >>> stevea_browser.getControl("Change").click()
719-
720- >>> stevea_browser.open(
721- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
722- >>> print_subscribers_from_duplicates(stevea_browser.contents)
723- From duplicates:
724- Steve Alexander (Subscribed to bug 2 by Launchpad Janitor)
725- (Unsubscribe Steve Alexander)
726-
727- >>> stevea_browser.getLink(id='unsubscribe-subscriber-11').mech_link.url
728- '+subscribe'
729+ >>> bug_2 = 'http://launchpad.dev/tomcat/+bug/2/'
730+ >>> stevea_browser.open(bug_2 + "+duplicate")
731+
732+ >>> stevea_browser.getControl("Duplicate Of").value = "3"
733+ >>> stevea_browser.getControl("Change").click()
734+
735+ >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
736+ >>> print_subscribers_from_duplicates(stevea_browser.contents)
737+ From duplicates:
738+ Steve Alexander (Subscribed to bug 2 by Launchpad Janitor)
739+ (Unsubscribe Steve Alexander)
740+
741+ >>> stevea_browser.getLink(id='unsubscribe-subscriber-11').mech_link.url
742+ '+subscribe'
743
744 When he chooses to unsubscribe, he will be unsubscribed from bug #2, the
745 dupe of bug #3, so he'll no longer get mail from bug #3.
746
747- >>> stevea_browser.open("http://launchpad.dev/bugs/3")
748- >>> stevea_browser.getLink('Unsubscribe').click()
749- >>> stevea_browser.getControl("Continue").click()
750+ >>> stevea_browser.open("http://launchpad.dev/bugs/3")
751+ >>> stevea_browser.getLink('Unsubscribe').click()
752+ >>> stevea_browser.getControl("Continue").click()
753
754 # XXX: Brad Bollenbach 2006-09-27 bug=62634: Printing the tag here,
755 # instead of tag.string.
756
757- >>> for tag in find_tags_by_class(
758- ... stevea_browser.contents, 'informational message'):
759- ... print tag.renderContents()
760- You have been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
761+ >>> for tag in find_tags_by_class(
762+ ... stevea_browser.contents, 'informational message'):
763+ ... print tag.renderContents()
764+ You have been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
765
766 (Except for Mark, who has a structural subscription to the target,
767 there are no longer any indirect subscribers, because Steve was
768 unsubscribed from the dupes and thus is no longer indirectly subscribed
769 to bug #3.)
770
771- >>> stevea_browser.open(
772- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
773- >>> print_subscribers_from_duplicates(stevea_browser.contents)
774- From duplicates:
775+ >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
776+ >>> print_subscribers_from_duplicates(stevea_browser.contents)
777+ From duplicates:
778
779- >>> stevea_browser.open(
780- ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
781- >>> print_also_notified(stevea_browser.contents)
782- Also notified:
783+ >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
784+ >>> print_also_notified(stevea_browser.contents)
785+ Also notified:
786
787 Let's repeat this example, with Steve subscribed to two different dupes,
788 to see how the unsubscribe notification changes slightly, because he
789 gets unsubscribed from more than one duplicate.
790
791- >>> stevea_browser.open(
792- ... "http://launchpad.dev/firefox/+bug/1/+duplicate")
793- >>> stevea_browser.getControl("Duplicate Of").value = "3"
794- >>> stevea_browser.getControl("Change").click()
795+ >>> stevea_browser.open(
796+ ... "http://launchpad.dev/firefox/+bug/1/+duplicate")
797+ >>> stevea_browser.getControl("Duplicate Of").value = "3"
798+ >>> stevea_browser.getControl("Change").click()
799
800 (Resubscribe Steve to bug #2, because he was unsubscribed in the
801 previous example.)
802
803- >>> stevea_browser.open(
804- ... "http://launchpad.dev/tomcat/+bug/2/+addsubscriber")
805- >>> stevea_browser.getControl('Person').value = 'stevea'
806- >>> stevea_browser.getControl('Subscribe user').click()
807-
808- >>> stevea_browser.open(
809- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
810- >>> print_subscribers_from_duplicates(stevea_browser.contents)
811- From duplicates:
812- Sample Person (Subscribed ...)
813- Steve Alexander (Subscribed ...) (Unsubscribe Steve Alexander)
814- testing Spanish team (Subscribed to bug 1 by Foo Bar)
815-
816- >>> stevea_browser.open(
817- ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
818- >>> print_also_notified(stevea_browser.contents)
819- Also notified:
820-
821- >>> stevea_browser.open("http://launchpad.dev/bugs/3")
822- >>> stevea_browser.getLink("Unsubscribe").click()
823- >>> stevea_browser.getControl("Continue").click()
824-
825- >>> for tag in find_tags_by_class(
826- ... stevea_browser.contents, 'informational message'):
827- ... print tag.renderContents()
828- You have been unsubscribed from bug 3 and 2 duplicates (<a...#1</a>, <a...#2</a>)...
829+ >>> stevea_browser.open(bug_2 + "+addsubscriber")
830+ >>> stevea_browser.getControl('Person').value = 'stevea'
831+ >>> stevea_browser.getControl('Subscribe user').click()
832+
833+ >>> stevea_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
834+ >>> print_subscribers_from_duplicates(stevea_browser.contents)
835+ From duplicates:
836+ Sample Person (Subscribed ...)
837+ Steve Alexander (Subscribed ...) (Unsubscribe Steve Alexander)
838+ testing Spanish team (Subscribed to bug 1 by Foo Bar)
839+
840+ >>> stevea_browser.open(bug_3 + "+bug-portlet-subscribers-content")
841+ >>> print_also_notified(stevea_browser.contents)
842+ Also notified:
843+
844+ >>> stevea_browser.open("http://launchpad.dev/bugs/3")
845+ >>> stevea_browser.getLink("Unsubscribe").click()
846+ >>> stevea_browser.getControl("Continue").click()
847+
848+ >>> for tag in find_tags_by_class(
849+ ... stevea_browser.contents, 'informational message'):
850+ ... print tag.renderContents()
851+ You have been unsubscribed from bug 3 and 2 duplicates (<a...#1</a>, <a...#2</a>)...
852
853 (Let's undupe bug #1 from bug #3, since it's unneeded for the examples
854 that follow.)
855
856- >>> stevea_browser.open(
857- ... "http://launchpad.dev/firefox/+bug/1/+duplicate")
858- >>> stevea_browser.getControl("Duplicate Of").value = ""
859- >>> stevea_browser.getControl("Change").click()
860+ >>> stevea_browser.open(
861+ ... "http://launchpad.dev/firefox/+bug/1/+duplicate")
862+ >>> stevea_browser.getControl("Duplicate Of").value = ""
863+ >>> stevea_browser.getControl("Change").click()
864
865 This unsubscribe behaviour is team-aware too, so you can unsubscribe
866 your teams from a bug, even when the team's subscription comes from a
867@@ -296,76 +289,79 @@
868 bug #2, and notice how bug #3's indirect subscriptions are update to
869 include that team.
870
871- >>> foobar_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
872- >>> foobar_browser.open("http://launchpad.dev/bugs/2")
873- >>> foobar_browser.getLink('Subscribe someone else').click()
874- >>> foobar_browser.getControl("Person").value = "ubuntu-team"
875- >>> foobar_browser.getControl("Subscribe user").click()
876-
877- >>> foobar_browser.open(
878- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
879-
880- >>> print_subscribers_from_duplicates(foobar_browser.contents)
881- From duplicates:
882- Ubuntu Team (Subscribed to bug 2 by Foo Bar) (Unsubscribe Ubuntu Team)
883-
884- >>> foobar_browser.open(
885- ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
886-
887- >>> print_also_notified(foobar_browser.contents)
888- Also notified:
889+ >>> foobar_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test")
890+ >>> foobar_browser.open("http://launchpad.dev/bugs/2")
891+ >>> foobar_browser.getLink('Subscribe someone else').click()
892+ >>> foobar_browser.getControl("Person").value = "ubuntu-team"
893+ >>> foobar_browser.getControl("Subscribe user").click()
894+
895+ >>> foobar_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
896+
897+ >>> print_subscribers_from_duplicates(foobar_browser.contents)
898+ From duplicates:
899+ Ubuntu Team (Subscribed to bug 2 by Foo Bar) (Unsubscribe Ubuntu Team)
900+
901+ >>> foobar_browser.open(
902+ ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
903+
904+ >>> print_also_notified(foobar_browser.contents)
905+ Also notified:
906
907 The subscribe link for Foo Bar still says "Subscribe", because
908 Foo Bar can subscribe himself directly to this bug. For unsubscribing
909 the team, the (-) icon can be used. In reality, the two links point to
910 the same page, but that is changed when the page is AJAX enabled.
911
912- >>> foobar_browser.open("http://launchpad.dev/bugs/3")
913- >>> foobar_browser.getLink('Subscribe').click()
914+ >>> foobar_browser.open("http://launchpad.dev/bugs/3")
915+ >>> foobar_browser.getLink('Subscribe').click()
916
917 Foo Bar can unsubscribe ubuntu-team, and ubuntu-team will no longer show
918 up in the indirect subscriptions.
919
920- >>> subscription_field = foobar_browser.getControl(name="field.subscription")
921- >>> subscription_field.value = ["ubuntu-team"]
922- >>> foobar_browser.getControl("Continue").click()
923+ >>> subscription_field = foobar_browser.getControl(
924+ ... name="field.subscription")
925+ >>> subscription_field.value = ["ubuntu-team"]
926+ >>> foobar_browser.getControl("Continue").click()
927
928- >>> for tag in find_tags_by_class(
929- ... foobar_browser.contents, 'informational message'):
930- ... print tag.renderContents()
931- Ubuntu Team has been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
932+ >>> for tag in find_tags_by_class(
933+ ... foobar_browser.contents, 'informational message'):
934+ ... print tag.renderContents()
935+ Ubuntu Team has been unsubscribed from bug 3 and 1 duplicate (<a...#2</a>)...
936
937 (ubuntu-team is no longer an indirect subscriber.)
938
939- >>> foobar_browser.open(
940- ... "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content")
941- >>> print_subscribers_from_duplicates(foobar_browser.contents)
942- From duplicates:
943-
944- >>> foobar_browser.open(
945- ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
946- >>> print_also_notified(foobar_browser.contents)
947- Also notified:
948-
949-
950-== Displaying subscribers ==
951+ >>> foobar_browser.open(bug_3 + "+bug-portlet-dupe-subscribers-content")
952+ >>> print_subscribers_from_duplicates(foobar_browser.contents)
953+ From duplicates:
954+
955+ >>> foobar_browser.open(
956+ ... "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content")
957+ >>> print_also_notified(foobar_browser.contents)
958+ Also notified:
959+
960+
961+Displaying subscribers
962+----------------------
963
964 The display names of subscribers are escaped in the subscribers list, they are
965-also trimmed to 20 characters, so that they fit alongside the unsubscribe icon.
966-
967- >>> login(ANONYMOUS)
968- >>> abuser = factory.makePerson(
969- ... name='abuser',
970- ... displayname='<script>javascript:alert("YO")</script>')
971- >>> logout()
972- >>> browser.open('http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
973- >>> browser.getControl('Person').value = 'abuser'
974- >>> browser.getControl('Subscribe user').click()
975- >>> browser.open(
976- ... 'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
977- >>> subscriber_list = find_tag_by_id(
978- ... browser.contents, 'subscribers-direct')
979- >>> for subscriber in subscriber_list.findAll('div'):
980- ... if '~abuser' in subscriber.a['href']:
981- ... print subscriber.a.contents[2].strip()
982- &lt;script&gt;javascrip...
983+also trimmed to 20 characters, so that they fit alongside the unsubscribe
984+icon.
985+
986+ >>> login(ANONYMOUS)
987+ >>> abuser = factory.makePerson(
988+ ... name='abuser',
989+ ... displayname='<script>javascript:alert("YO")</script>')
990+ >>> logout()
991+ >>> browser.open(
992+ ... 'http://bugs.launchpad.dev/firefox/+bug/1/+addsubscriber')
993+ >>> browser.getControl('Person').value = 'abuser'
994+ >>> browser.getControl('Subscribe user').click()
995+ >>> bug_1 = 'http://bugs.launchpad.dev/bugs/1/'
996+ >>> browser.open(bug_1 + '+bug-portlet-subscribers-content')
997+ >>> subscriber_list = find_tag_by_id(
998+ ... browser.contents, 'subscribers-direct')
999+ >>> for subscriber in subscriber_list.findAll('div'):
1000+ ... if '~abuser' in subscriber.a['href']:
1001+ ... print subscriber.a.contents[2].strip()
1002+ &lt;script&gt;javascrip...
1003+
1004
1005=== modified file 'lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt'
1006--- lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt 2011-03-10 17:03:32 +0000
1007+++ lib/lp/bugs/stories/xx-bugs-statistics-portlet.txt 2011-05-25 16:14:50 +0000
1008@@ -1,4 +1,5 @@
1009-= Bug statistics portlet =
1010+Bug statistics portlet
1011+======================
1012
1013 The distribution, project group and project bug listings contain a
1014 portlet that shows bug statistics for the target. Each statistic is a
1015@@ -6,7 +7,8 @@
1016 served in a separate request; the request is issued via Javascript and
1017 inserted into the page later.
1018
1019-== Distribution ==
1020+Distribution
1021+------------
1022
1023 >>> path = 'debian'
1024
1025@@ -77,7 +79,8 @@
1026 http://bugs.launchpad.dev/debian/+cve
1027
1028
1029-== Distribution Series ==
1030+Distribution Series
1031+-------------------
1032
1033 >>> path = 'debian/woody'
1034
1035@@ -149,7 +152,8 @@
1036 http://bugs.launchpad.dev/debian/woody/+cve
1037
1038
1039-== Distribution Source Package ==
1040+Distribution Source Package
1041+---------------------------
1042
1043 >>> path = 'debian/+source/mozilla-firefox'
1044
1045@@ -219,7 +223,8 @@
1046 LinkNotFoundError
1047
1048
1049-== Source Package in Distribution Series ==
1050+Source Package in Distribution Series
1051+-------------------------------------
1052
1053 >>> path = 'debian/woody/+source/mozilla-firefox'
1054
1055@@ -286,7 +291,8 @@
1056 LinkNotFoundError
1057
1058
1059-== Project group ==
1060+Project group
1061+-------------
1062
1063 >>> path = 'mozilla'
1064
1065@@ -356,7 +362,8 @@
1066 LinkNotFoundError
1067
1068
1069-== Project ==
1070+Project
1071+-------
1072
1073 >>> path = 'firefox'
1074