~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/doc/bugnotification-email.txt

  • Committer: Jeroen Vermeulen
  • Date: 2011-09-19 06:57:55 UTC
  • mto: This revision was merged to the branch mainline in revision 13994.
  • Revision ID: jeroen.vermeulen@canonical.com-20110919065755-lgot1hi4xfqrf492
Lint.  Lots of lint.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
== Bug Notification Email ==
 
1
Bug Notification Email
 
2
----------------------
2
3
 
3
4
This document describes the internal workings of how bug notification
4
5
emails are generated and how said emails are formatted. It does not
22
23
    >>> from lp.bugs.adapters.bugchange import get_bug_changes
23
24
    >>> from lp.bugs.mail.newbug import generate_bug_add_email
24
25
 
25
 
Let's demonstrate what the bugmails will look like, by going through
26
 
the various events that can happen that would cause a notification to
27
 
be sent. We'll start by importing some things we'll need for the
28
 
examples that follow:
 
26
Let's demonstrate what the bugmails will look like, by going through the
 
27
various events that can happen that would cause a notification to be
 
28
sent. We'll start by importing some things we'll need for the examples
 
29
that follow:
29
30
 
30
31
    >>> from zope.component import getUtility
31
 
    >>> from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
 
32
    >>> from canonical.launchpad.interfaces.emailaddress import (
 
33
    ...     IEmailAddressSet)
32
34
    >>> from lp.bugs.adapters.bugdelta import BugDelta
33
35
    >>> from lp.bugs.interfaces.bug import (
34
 
    ...     IBugDelta,
35
 
    ...     IBugSet,
36
 
    ...     )
 
36
    ...        IBugDelta,
 
37
    ...        IBugSet,
 
38
    ...        )
37
39
    >>> from lp.registry.interfaces.person import IPersonSet
38
40
 
39
 
= Filing a bug =
 
41
 
 
42
Filing a bug
 
43
============
40
44
 
41
45
generate_bug_add_email accepts one argument: the IBug that was just
42
46
added. With that, it generates an appropriately-formatted notification
52
56
    >>> subject, body = generate_bug_add_email(bug_four)
53
57
    >>> subject
54
58
    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
 
59
 
55
60
    >>> print body
56
61
    Public bug reported:
57
62
    <BLANKLINE>
73
78
    >>> subject, body = generate_bug_add_email(bug_four)
74
79
    >>> subject
75
80
    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
 
81
 
76
82
    >>> print body
77
83
    Public bug reported:
78
84
    <BLANKLINE>
87
93
 
88
94
New security related bugs are sent with a prominent warning:
89
95
 
90
 
    >>> changed = bug_four.setSecurityRelated(True, getUtility(ILaunchBag).user)
 
96
    >>> changed = bug_four.setSecurityRelated(
 
97
    ...     True, getUtility(ILaunchBag).user)
91
98
 
92
99
    >>> subject, body = generate_bug_add_email(bug_four)
93
100
    >>> subject
94
101
    u'[Bug 4] [NEW] Reflow problems with complex page layouts'
 
102
 
95
103
    >>> print body
96
104
    *** This bug is a security vulnerability ***
97
105
    <BLANKLINE>
113
121
    ...
114
122
 
115
123
 
116
 
= Editing a bug =
 
124
Editing a bug
 
125
=============
117
126
 
118
127
get_bug_changes() accepts an object that provides IBugDelta, and
119
128
generates IBugChange objects that describe the changes to the bug.
125
134
    >>> edited_bug.title = "the new title"
126
135
    >>> old_description = edited_bug.description
127
136
    >>> edited_bug.description = (
128
 
    ...     "The Trash folder seems to have significant problems! At the"
129
 
    ...     " moment, dragging an item to the Trash results in immediate"
130
 
    ...     " deletion. The item does not appear in the Trash, it is just"
131
 
    ...     " deleted from my hard disk. There is no undo or ability to"
132
 
    ...     " recover the deleted file. Help!")
 
137
    ...        "The Trash folder seems to have significant problems! At the"
 
138
    ...        " moment, dragging an item to the Trash results in immediate"
 
139
    ...        " deletion. The item does not appear in the Trash, it is just"
 
140
    ...        " deleted from my hard disk. There is no undo or ability to"
 
141
    ...        " recover the deleted file. Help!")
133
142
 
134
143
    >>> bug_delta = BugDelta(
135
 
    ...     bug=edited_bug,
136
 
    ...     bugurl="http://www.example.com/bugs/2",
137
 
    ...     user=sample_person,
138
 
    ...     title={'new': edited_bug.title, 'old': old_title},
139
 
    ...     description={'new': edited_bug.description,
140
 
    ...                  'old': old_description})
 
144
    ...        bug=edited_bug,
 
145
    ...        bugurl="http://www.example.com/bugs/2",
 
146
    ...        user=sample_person,
 
147
    ...        title={'new': edited_bug.title, 'old': old_title},
 
148
    ...        description={'new': edited_bug.description,
 
149
    ...                     'old': old_description})
141
150
    >>> IBugDelta.providedBy(bug_delta)
142
151
    True
143
152
 
144
153
    >>> from lp.bugs.interfaces.bugchange import IBugChange
145
154
    >>> changes = get_bug_changes(bug_delta)
146
155
    >>> for change in changes:
147
 
    ...     IBugChange.providedBy(change)
 
156
    ...        IBugChange.providedBy(change)
148
157
    True
149
158
    True
150
159
 
151
160
    >>> for change in get_bug_changes(bug_delta):
152
 
    ...     notification = change.getBugNotification()
153
 
    ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
154
 
    ...     print "-----------------------------"
 
161
    ...        notification = change.getBugNotification()
 
162
    ...        print notification['text'] #doctest: -NORMALIZE_WHITESPACE
 
163
    ...        print "-----------------------------"
155
164
    ** Summary changed:
156
165
    <BLANKLINE>
157
166
    - Blackhole Trash folder
170
179
is wrapped properly:
171
180
 
172
181
    >>> old_description = edited_bug.description
173
 
    >>> edited_bug.description = """\
174
 
    ... a new description that is quite long. but the nice thing is that the edit notification email generator knows how to indent and wrap descriptions, so this will appear quite nice in the actual email that gets sent.\n\nit's also smart enough to preserve whitespace, finally!"""
 
182
    >>> edited_bug.description = ''.join([
 
183
    ...     "A new description that is quite long. ",
 
184
    ...     "But the nice thing is that the edit notification email ",
 
185
    ...     "generator knows how to indent and wrap descriptions, so this ",
 
186
    ...     "will appear quite nice in the actual email that gets sent.",
 
187
    ...     "\n",
 
188
    ...     "\n",
 
189
    ...     "It's also smart enough to preserve whitespace, finally!",
 
190
    ...     ])
175
191
 
176
192
    >>> bug_delta = BugDelta(
177
193
    ...     bug=edited_bug,
178
194
    ...     bugurl="http://www.example.com/bugs/2",
179
195
    ...     user=sample_person,
180
 
    ...     description={'new': edited_bug.description,
181
 
    ...                  'old': old_description})
 
196
    ...     description={
 
197
    ...         'new': edited_bug.description,
 
198
    ...         'old': old_description,
 
199
    ...     })
182
200
    >>> for change in get_bug_changes(bug_delta):
183
201
    ...     notification = change.getBugNotification()
184
202
    ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
189
207
    - dragging an item to the Trash results in immediate deletion. The item
190
208
    - does not appear in the Trash, it is just deleted from my hard disk.
191
209
    - There is no undo or ability to recover the deleted file. Help!
192
 
    + a new description that is quite long. but the nice thing is that the
 
210
    + A new description that is quite long. But the nice thing is that the
193
211
    + edit notification email generator knows how to indent and wrap
194
212
    + descriptions, so this will appear quite nice in the actual email that
195
213
    + gets sent.
196
214
    + 
197
 
    + it's also smart enough to preserve whitespace, finally!
 
215
    + It's also smart enough to preserve whitespace, finally!
198
216
    -----------------------------
199
217
 
200
218
Let's make the bug security-related, and private (we need to switch
204
222
 
205
223
    >>> edited_bug.setPrivate(True, getUtility(ILaunchBag).user)
206
224
    True
207
 
    >>> changed = edited_bug.setSecurityRelated(True, getUtility(ILaunchBag).user)
 
225
 
 
226
    >>> changed = edited_bug.setSecurityRelated(
 
227
    ...     True, getUtility(ILaunchBag).user)
208
228
    >>> bug_delta = BugDelta(
209
229
    ...     bug=edited_bug,
210
230
    ...     bugurl="http://www.example.com/bugs/2",
211
231
    ...     user=sample_person,
212
232
    ...     private={'old': False, 'new': edited_bug.private},
213
 
    ...     security_related={'old': False, 'new': edited_bug.security_related})
 
233
    ...     security_related={
 
234
    ...         'old': False,
 
235
    ...         'new': edited_bug.security_related,
 
236
    ...     })
214
237
 
215
238
    >>> for change in get_bug_changes(bug_delta):
216
239
    ...     notification = change.getBugNotification()
227
250
 
228
251
    >>> edited_bug.setPrivate(False, getUtility(ILaunchBag).user)
229
252
    True
230
 
    >>> changed = edited_bug.setSecurityRelated(False, getUtility(ILaunchBag).user)
 
253
 
 
254
    >>> changed = edited_bug.setSecurityRelated(
 
255
    ...     False, getUtility(ILaunchBag).user)
231
256
    >>> bug_delta = BugDelta(
232
257
    ...     bug=edited_bug,
233
258
    ...     bugurl="http://www.example.com/bugs/2",
234
259
    ...     user=sample_person,
235
260
    ...     private={'old': True, 'new': edited_bug.private},
236
 
    ...     security_related={'old': True, 'new': edited_bug.security_related})
 
261
    ...     security_related={
 
262
    ...         'old': True,
 
263
    ...         'new': edited_bug.security_related,
 
264
    ...         })
237
265
    >>> for change in get_bug_changes(bug_delta):
238
266
    ...     notification = change.getBugNotification()
239
267
    ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
248
276
    >>> old_tags = []
249
277
    >>> edited_bug.tags = [u'foo', u'bar']
250
278
    >>> bug_delta = BugDelta(
251
 
    ...     bug=edited_bug,
252
 
    ...     bugurl="http://www.example.com/bugs/2",
253
 
    ...     user=sample_person,
254
 
    ...     tags={'old': old_tags, 'new': edited_bug.tags})
 
279
    ...        bug=edited_bug,
 
280
    ...        bugurl="http://www.example.com/bugs/2",
 
281
    ...        user=sample_person,
 
282
    ...        tags={'old': old_tags, 'new': edited_bug.tags})
255
283
    >>> for change in get_bug_changes(bug_delta):
256
 
    ...     notification = change.getBugNotification()
257
 
    ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
258
 
    ...     print "-----------------------------"
 
284
    ...        notification = change.getBugNotification()
 
285
    ...        print notification['text'] #doctest: -NORMALIZE_WHITESPACE
 
286
    ...        print "-----------------------------"
259
287
    ** Tags added: bar foo
260
288
    -----------------------------
261
289
 
264
292
    >>> old_tags = edited_bug.tags
265
293
    >>> edited_bug.tags = [u'foo', u'baz']
266
294
    >>> bug_delta = BugDelta(
267
 
    ...     bug=edited_bug,
268
 
    ...     bugurl="http://www.example.com/bugs/2",
269
 
    ...     user=sample_person,
270
 
    ...     tags={'old': old_tags, 'new': edited_bug.tags})
 
295
    ...        bug=edited_bug,
 
296
    ...        bugurl="http://www.example.com/bugs/2",
 
297
    ...        user=sample_person,
 
298
    ...        tags={'old': old_tags, 'new': edited_bug.tags})
271
299
    >>> for change in get_bug_changes(bug_delta):
272
 
    ...     notification = change.getBugNotification()
273
 
    ...     print notification['text'] #doctest: -NORMALIZE_WHITESPACE
274
 
    ...     print "-----------------------------"
 
300
    ...        notification = change.getBugNotification()
 
301
    ...        print notification['text'] #doctest: -NORMALIZE_WHITESPACE
 
302
    ...        print "-----------------------------"
275
303
    ** Tags removed: bar
276
304
    ** Tags added: baz
277
305
    -----------------------------
278
306
 
279
307
 
280
 
= Editing a bug task =
 
308
Editing a bug task
 
309
==================
281
310
 
282
 
As you might expect, get_bug_changes handles generating the
283
 
text representations of the changes when a bug task is edited.
 
311
As you might expect, get_bug_changes handles generating the text
 
312
representations of the changes when a bug task is edited.
284
313
 
285
314
We use a BugTaskDelta to represent changes to a BugTask.
286
315
 
287
316
    >>> from canonical.launchpad.webapp.testing import verifyObject
288
317
    >>> from lp.bugs.interfaces.bugtask import (
289
 
    ...     BugTaskStatus,
290
 
    ...     IBugTaskDelta,
291
 
    ...     IBugTaskSet,
292
 
    ...     )
 
318
    ...        BugTaskStatus,
 
319
    ...        IBugTaskDelta,
 
320
    ...        IBugTaskSet,
 
321
    ...        )
293
322
    >>> from lp.bugs.model.bugtask import BugTaskDelta
294
323
    >>> example_bug_task = factory.makeBugTask()
295
324
    >>> example_delta = BugTaskDelta(example_bug_task)
298
327
 
299
328
    >>> edited_bugtask = getUtility(IBugTaskSet).get(3)
300
329
    >>> edited_bugtask.transitionToStatus(
301
 
    ...     BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
 
330
    ...        BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
302
331
    >>> edited_bugtask.transitionToAssignee(sample_person)
303
332
    >>> bugtask_delta = BugTaskDelta(
304
333
    ...     bugtask=edited_bugtask,
326
355
    >>> debian_bugtask = getUtility(IBugTaskSet).get(5)
327
356
    >>> print debian_bugtask.bugtargetname
328
357
    mozilla-firefox (Debian)
 
358
 
329
359
    >>> debian_bugtask.transitionToAssignee(None)
330
360
    >>> bugtask_delta = BugTaskDelta(
331
361
    ...     bugtask=debian_bugtask,
344
374
    -----------------------------
345
375
 
346
376
 
347
 
= Adding attachments =
 
377
Adding attachments
 
378
==================
348
379
 
349
380
Adding an attachment will generate a notification that looks as follows:
350
381
 
414
445
    -----------------------------
415
446
 
416
447
 
417
 
= Generation of From: and Reply-To: addresses =
 
448
Generation of From: and Reply-To: addresses
 
449
===========================================
418
450
 
419
451
The Reply-To: and From: addresses used to send email are generated in a
420
452
pair of handy functions defined in mailnotification.py:
432
464
 
433
465
    >>> stub = getUtility(IPersonSet).getByName("stub")
434
466
    >>> [(email.email, email.status.name) for email
435
 
    ...  in getUtility(IEmailAddressSet).getByPerson(stub)]
 
467
    ...     in getUtility(IEmailAddressSet).getByPerson(stub)]
436
468
    [(u'stuart.bishop@canonical.com', 'PREFERRED'),
437
469
     (u'stuart@stuartbishop.net', 'VALIDATED'),
438
470
     (u'stub@fastmail.fm', 'NEW'),
479
511
    'Ford Prefect <4@bugs.launchpad.net>'
480
512
 
481
513
 
482
 
== Construction of bug notification emails ==
 
514
Construction of bug notification emails
 
515
---------------------------------------
483
516
 
484
517
mailnotification.py contains a class, BugNotificationBuilder, which is
485
518
used to construct bug notification emails.
486
519
 
487
520
    >>> from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
488
521
 
489
 
When instantiatiated it derives a list of common unchanging headers
490
 
from the bug so that they are not calculated for every recipient.
 
522
When instantiatiated it derives a list of common unchanging headers from
 
523
the bug so that they are not calculated for every recipient.
491
524
 
492
525
    >>> bug_four_notification_builder = BugNotificationBuilder(bug_four,
493
526
    ...     private_person)
504
537
    X-Launchpad-Bug-Modifier: Ford Prefect (person-name...)
505
538
 
506
539
The build() method of a builder accepts a number of parameters and
507
 
returns an instance of email.MIMEText. The most basic invocation of
508
 
this method requires a from address, a to address, a body, a subject
509
 
and a sending date for the mail.
 
540
returns an instance of email.MIMEText. The most basic invocation of this
 
541
method requires a from address, a to address, a body, a subject and a
 
542
sending date for the mail.
510
543
 
511
544
    >>> from datetime import datetime
512
545
    >>> import pytz
519
552
    ...     from_address, 'foo.bar@canonical.com',
520
553
    ...     "A test body.", "A test subject.", sending_date)
521
554
 
522
 
The fields of the generated notification email will be set according
523
 
to the parameters that were used to instantiate BugNotificationBuilder
524
 
and passed to <builder>.build().
 
555
The fields of the generated notification email will be set according to
 
556
the parameters that were used to instantiate BugNotificationBuilder and
 
557
passed to <builder>.build().
525
558
 
526
559
    >>> print notification_email['From']
527
560
    Launchpad Bug Tracker <4@bugs.launchpad.net>