~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
BugNotificationRecipients instances store email addresses mapped to the
reason for which they are being notified in a certain bug. It implements
the INotificationRecipientSet interface for bug notifications. (See
notification-recipient-set.txt for the details.)

How it's used
=============

The IBug.getBugNotificationRecipients() implementation creates and uses
BugNotificationRecipients instances. Here's an example of this in
action:

    >>> from lp.bugs.model.bug import Bug
    >>> from lp.registry.model.distribution import Distribution
    >>> from lp.registry.model.product import Product
    >>> bug_one = Bug.get(1)
    >>> recipients = bug_one.getBugNotificationRecipients()

The instance of BugNotificationRecipients we get back correctly
implements INotificationRecipientSet:

    >>> from canonical.launchpad.interfaces.launchpad import (
    ...     INotificationRecipientSet,
    ...     )
    >>> from canonical.launchpad.webapp.testing import verifyObject
    >>> verifyObject(INotificationRecipientSet, recipients)
    True

This instance contains email addresses and rationales. Let's define a
helper function so we can format this output:

    >>> def print_rationales(rationale):
    ...   for address in rationale.getEmails():
    ...     text, header = rationale.getReason(address)
    ...     print address
    ...     print "    %s" % header
    ...     print "    %s" % text

And print them out. The first line is the email address; second is the
text appropriate to be used in an X- header, and the last is the text
appropriate for an email footer.

    >>> print_rationales(recipients)
    foo.bar@canonical.com
        Subscriber (mozilla-firefox in Ubuntu)
        You received this bug notification because you are subscribed
        to mozilla-firefox in Ubuntu.
    mark@example.com
        Assignee
        You received this bug notification because you are a bug assignee.
    support@ubuntu.com
        Registrant (Ubuntu) @ubuntu-team
        You received this bug notification because you are a member of
        Ubuntu Team, which is the registrant for Ubuntu.
    test@canonical.com
        Subscriber
        You received this bug notification because you are a direct
        subscriber of the bug.

The Bug-BugNotificationRecipients API
=====================================

Most of the API of BugNotificationRecipients is actually kept private
between the Bug class and itself. Let's now demonstrate the API that Bug
and BugNotificationRecipients use to set up the rationales; this is
essentially what happens under the wraps when you call
IBug.getBugNotificationRecipients().

Let's up some data for our test:

    >>> from lp.registry.interfaces.person import IPersonSet
    >>> from lp.bugs.mail.bugnotificationrecipients import (
    ...     BugNotificationRecipients)
    >>> debian = Distribution.selectOneBy(name="debian")
    >>> pmount = debian.getSourcePackage("pmount")
    >>> alsa_utils = Product.selectOneBy(name="alsa-utils")
    >>> gnomebaker = Product.selectOneBy(name="gnomebaker")
    >>> personset = getUtility(IPersonSet)

Here's where getBugNotificationRecipients() starts off. First, a
BugNotificationRecipients instance is created:

    >>> recipients = BugNotificationRecipients()

Then, subscribers of various types are added:

    >>> foo_bar = personset.getByEmail("foo.bar@canonical.com")
    >>> recipients.addDupeSubscriber(foo_bar)

    >>> test = personset.getByEmail("test@canonical.com")
    >>> recipients.addDirectSubscriber(test)

    >>> no_priv = personset.getByEmail("no-priv@canonical.com")
    >>> recipients.addAssignee(no_priv)

    >>> carlos = personset.getByEmail("carlos@canonical.com")
    >>> recipients.addStructuralSubscriber(carlos, pmount)

    >>> testtest = personset.getByEmail("testtest@canonical.com")
    >>> recipients.addRegistrant(testtest, gnomebaker)

If we print out the recipients and rationales, here's what we get:

    >>> print_rationales(recipients)
    carlos@canonical.com
        Subscriber (pmount in Debian)
        You received this bug notification because you are subscribed
        to pmount in Debian.
    foo.bar@canonical.com
        Subscriber of Duplicate
        You received this bug notification because you are a direct
        subscriber of a duplicate bug.
    no-priv@canonical.com
        Assignee
        You received this bug notification because you are a bug
        assignee.
    test@canonical.com
        Subscriber
        You received this bug notification because you are a direct
        subscriber of the bug.

Note how we account for every important variation in bug subscriptions
here: bug supervisors, subscribers, dupe subscribers and more.

A duplicate bug modification notifies its main bug
==================================================

If the bug we are changing is actually a duplicate of another bug, an
additional step is involved. A BugNotificationRecipients instance is
created, annotating that it represents a master bug (of which we are a
duplicate of).

    >>> bug_two = Bug.get(2)
    >>> recipients = BugNotificationRecipients(duplicateof=bug_two)

    >>> foo_bar = personset.getByEmail("foo.bar@canonical.com")
    >>> recipients.addDupeSubscriber(foo_bar)

    >>> test = personset.getByEmail("test@canonical.com")
    >>> recipients.addDirectSubscriber(test)

    >>> no_priv = personset.getByEmail("no-priv@canonical.com")
    >>> recipients.addAssignee(no_priv)

    >>> carlos = personset.getByEmail("carlos@canonical.com")
    >>> recipients.addStructuralSubscriber(carlos, pmount)

    >>> testtest = personset.getByEmail("testtest@canonical.com")
    >>> recipients.addRegistrant(testtest, gnomebaker)

If you print out rationales in this situation, you'll see that the
message says "via Bug 2". The reason for this is that the people being
notified here are actually subscribed to bug 2, and they may be asking
themselves why the hell they are getting email for bug 1.

    >>> print_rationales(recipients)
    carlos@canonical.com
        Subscriber (pmount in Debian) via Bug 2
        You received this bug notification because you are subscribed
        to pmount in Debian (via bug 2).
    foo.bar@canonical.com
        Subscriber of Duplicate via Bug 2
        You received this bug notification because you are a direct
        subscriber of a duplicate bug (via bug 2).
    no-priv@canonical.com
        Assignee via Bug 2
        You received this bug notification because you are a bug
        assignee (via bug 2).
    test@canonical.com
        Subscriber via Bug 2
        You received this bug notification because you are a direct
        subscriber of the bug (via bug 2).

Team subscribers are special
============================

In the case where the teams are subscribers, things vary according to
whether the team has a contact email address or not. When there is no
contact email address, all team members (cascaded down) get emailed
directly, and the person getting the notification may not know of this
immediately.

Here's an example of this situation:

    >>> recipients = BugNotificationRecipients()
    >>> testing_spanish_team = personset.getByName("testing-spanish-team")
    >>> recipients.addDupeSubscriber(testing_spanish_team)

    >>> guadamen = personset.getByName("guadamen")
    >>> recipients.addAssignee(guadamen)

    >>> name20 = personset.getByName("name20")
    >>> recipients.addStructuralSubscriber(name20, pmount)

    >>> vcs_imports = personset.getByName("vcs-imports")
    >>> recipients.addRegistrant(vcs_imports, gnomebaker)

    >>> commercial_admins = personset.getByName("commercial-admins")
    >>> recipients.addDirectSubscriber(commercial_admins)

You'll notice that the rationales this time state clearly which team
membership is causing us to send mail.

    >>> print_rationales(recipients)
      carlos@canonical.com
          Subscriber of Duplicate @testing-spanish-team
          You received this bug notification because you are a member
          of testing Spanish team, which is a subscriber of a
          duplicate bug.
      commercial-member@canonical.com
          Subscriber @commercial-admins
          You received this bug notification because you are a member
          of Commercial Subscription Admins, which is a direct subscriber.
      david.allouche@canonical.com
          Registrant (gnomebaker) @vcs-imports
          You received this bug notification because you are a member
          of VCS imports, which is the registrant for gnomebaker.
      foo.bar@canonical.com
          Subscriber of Duplicate @testing-spanish-team
          You received this bug notification because you are a member
          of testing Spanish team, which is a subscriber of a
          duplicate bug.
      kurem@debian.cz
          Subscriber of Duplicate @testing-spanish-team
          You received this bug notification because you are a member
          of testing Spanish team, which is a subscriber of a
          duplicate bug.
      mark@example.com
          Subscriber of Duplicate @testing-spanish-team
          You received this bug notification because you are a member
          of testing Spanish team, which is a subscriber of a
          duplicate bug.
      robertc@robertcollins.net
          Registrant (gnomebaker) @vcs-imports
          You received this bug notification because you are a member
          of VCS imports, which is the registrant for gnomebaker.
      support@ubuntu.com
          Assignee @guadamen
          You received this bug notification because you are a member
          of GuadaMen, which is a bug assignee.
      test@canonical.com
          Subscriber (pmount in Debian) @name20
          You received this bug notification because you are a member
          of Warty Security Team, which is subscribed to pmount in
          Debian.
      tsukimi@quaqua.net
          Subscriber of Duplicate @testing-spanish-team
          You received this bug notification because you are a member
          of testing Spanish team, which is a subscriber of a
          duplicate bug.

This doesn't help the end-user too much if he's a member of this team
indirectly (for instance, if he's a member of a team which is in turn a
member of another team); however, in that case, the user can still visit
the team page and see the membership graph directly. This may be worth
fixing in the future.

First impressions stick
=======================

Another important property of BugNotificationRecipients is that the
first rationale presented to it is the one that is presented -- even if
the recipient has multiple reasons for which he might be emailed. Here's
a pathological example:

    >>> recipients = BugNotificationRecipients()
    >>> recipients.addDirectSubscriber(test)
    >>> recipients.addAssignee(test)
    >>> recipients.addRegistrant(test, gnomebaker)
    >>> recipients.addDirectSubscriber(foo_bar)

This guy is emailed because he's a direct subscriber, an assignee and an
upstream registrant. However, if we ask the rationales instance:

    >>> print_rationales(recipients)
    foo.bar@canonical.com
        Subscriber
        You received this bug notification because you are a direct
        subscriber of the bug.
    test@canonical.com
        Subscriber
        You received this bug notification because you are a direct
        subscriber of the bug.

Only the first rationale is presented. This is the case even if we
update this set of recipients with another one:

    >>> recipients2 = BugNotificationRecipients()
    >>> recipients2.addDupeSubscriber(test)
    >>> recipients2.update(recipients)

The rationales for test@canonical.com in the 'recipients' instance just
don't matter:

    >>> print_rationales(recipients2)
    foo.bar@canonical.com
        Subscriber
        You received this bug notification because you are a direct
        subscriber of the bug.
    test@canonical.com
        Subscriber of Duplicate
        You received this bug notification because you are a direct
        subscriber of a duplicate bug.

This may be seen as a limitation, but you don't want a 10-line rationale
footer for people who are central to Launchpad, so for now it's the way
it is.