Merge lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11417
Proposed branch: lp:~allenap/launchpad/refactor-mailnotification
Merge into: lp:launchpad
Prerequisite: lp:~allenap/launchpad/refactor-get-email-notifications
Diff against target: 1160 lines (+465/-471)
11 files modified
lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt (+0/-7)
lib/canonical/launchpad/mailnotification.py (+4/-434)
lib/canonical/launchpad/subscribers/karma.py (+3/-1)
lib/lp/bugs/configure.zcml (+6/-9)
lib/lp/bugs/doc/bugnotification-email.txt (+3/-6)
lib/lp/bugs/doc/bugsubscription.txt (+2/-4)
lib/lp/bugs/mail/newbug.py (+95/-0)
lib/lp/bugs/scripts/bugnotification.py (+2/-4)
lib/lp/bugs/subscribers/bug.py (+268/-5)
lib/lp/bugs/subscribers/bugcreation.py (+3/-1)
lib/lp/bugs/subscribers/bugtask.py (+79/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/refactor-mailnotification
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+32923@code.launchpad.net

Commit message

Move most of the remaining bugs related code out of c.l.mailnotification and into the lp.bugs hierarchy, and remove a few unused functions.

Description of the change

Moves almost all of the remaining bug related code out of c/l/mailnotification.py. Also removes a few unused functions.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

This is almost entirely just moving code; I have avoided making changes to the logic.

Revision history for this message
Aaron Bentley (abentley) wrote :

Please fix the copyright date on newbug.py. Otherwise, this looks fine.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt'
2--- lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt 2005-10-31 18:29:12 +0000
3+++ lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt 1970-01-01 00:00:00 +0000
4@@ -1,7 +0,0 @@
5-The following email was unhandled:
6-
7-%(url)s
8-
9-Error message:
10-
11-%(error_msg)s
12
13=== modified file 'lib/canonical/launchpad/mailnotification.py'
14--- lib/canonical/launchpad/mailnotification.py 2010-08-20 20:31:18 +0000
15+++ lib/canonical/launchpad/mailnotification.py 2010-08-23 20:18:06 +0000
16@@ -8,7 +8,6 @@
17
18 __metaclass__ = type
19
20-import datetime
21 from difflib import unified_diff
22 from email.Header import Header
23 from email.MIMEMessage import MIMEMessage
24@@ -18,7 +17,6 @@
25 formataddr,
26 make_msgid,
27 )
28-import operator
29 import re
30
31 from zope.component import (
32@@ -37,9 +35,7 @@
33 IPerson,
34 IPersonSet,
35 ISpecification,
36- IStructuralSubscriptionTarget,
37 ITeamMembershipSet,
38- IUpstreamBugTask,
39 TeamMembershipStatus,
40 )
41 from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
42@@ -55,102 +51,20 @@
43 )
44 from canonical.launchpad.webapp.publisher import canonical_url
45 from canonical.launchpad.webapp.url import urlappend
46-from lp.bugs.adapters.bugchange import (
47- BugDuplicateChange,
48- BugTaskAssigneeChange,
49- get_bug_changes,
50- )
51-from lp.bugs.adapters.bugdelta import BugDelta
52-from lp.bugs.interfaces.bugchange import IBugChange
53-from lp.bugs.mail.bugnotificationbuilder import (
54- BugNotificationBuilder,
55- get_bugmail_error_address,
56- )
57-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
58-from lp.registry.enum import BugNotificationLevel
59+from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address
60 from lp.services.mail.mailwrapper import MailWrapper
61 # XXX 2010-06-16 gmb bug=594985
62 # This shouldn't be here, but if we take it out lots of things cry,
63 # which is sad.
64 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
65
66+# Silence lint warnings.
67+NotificationRecipientSet
68+
69
70 CC = "CC"
71
72
73-def _send_bug_details_to_new_bug_subscribers(
74- bug, previous_subscribers, current_subscribers, subscribed_by=None,
75- event_creator=None):
76- """Send an email containing full bug details to new bug subscribers.
77-
78- This function is designed to handle situations where bugtasks get
79- reassigned to new products or sourcepackages, and the new bug subscribers
80- need to be notified of the bug.
81- """
82- prev_subs_set = set(previous_subscribers)
83- cur_subs_set = set(current_subscribers)
84- new_subs = cur_subs_set.difference(prev_subs_set)
85-
86- to_addrs = set()
87- for new_sub in new_subs:
88- to_addrs.update(get_contact_email_addresses(new_sub))
89-
90- if not to_addrs:
91- return
92-
93- from_addr = format_address(
94- 'Launchpad Bug Tracker',
95- "%s@%s" % (bug.id, config.launchpad.bugs_domain))
96- # Now's a good a time as any for this email; don't use the original
97- # reported date for the bug as it will just confuse mailer and
98- # recipient.
99- email_date = datetime.datetime.now()
100-
101- # The new subscriber email is effectively the initial message regarding
102- # a new bug. The bug's initial message is used in the References
103- # header to establish the message's context in the email client.
104- references = [bug.initial_message.rfc822msgid]
105- recipients = bug.getBugNotificationRecipients()
106-
107- bug_notification_builder = BugNotificationBuilder(bug, event_creator)
108- for to_addr in sorted(to_addrs):
109- reason, rationale = recipients.getReason(to_addr)
110- subject, contents = generate_bug_add_email(
111- bug, new_recipients=True, subscribed_by=subscribed_by,
112- reason=reason, event_creator=event_creator)
113- msg = bug_notification_builder.build(
114- from_addr, to_addr, contents, subject, email_date,
115- rationale=rationale, references=references)
116- sendmail(msg)
117-
118-
119-@block_implicit_flushes
120-def update_security_contact_subscriptions(modified_bugtask, event):
121- """Subscribe the new security contact when a bugtask's product changes.
122-
123- Only subscribes the new security contact if the bug was marked a
124- security issue originally.
125-
126- No change is made for private bugs.
127- """
128- if event.object.bug.private:
129- return
130-
131- if not IUpstreamBugTask.providedBy(event.object):
132- return
133-
134- bugtask_before_modification = event.object_before_modification
135- bugtask_after_modification = event.object
136-
137- if (bugtask_before_modification.product !=
138- bugtask_after_modification.product):
139- new_product = bugtask_after_modification.product
140- if (bugtask_before_modification.bug.security_related and
141- new_product.security_contact):
142- bugtask_after_modification.bug.subscribe(
143- new_product.security_contact, IPerson(event.user))
144-
145-
146 def send_process_error_notification(to_address, subject, error_msg,
147 original_msg, failing_command=None):
148 """Send a mail about an error occurring while using the email interface.
149@@ -193,102 +107,6 @@
150 sendmail(msg)
151
152
153-def notify_errors_list(message, file_alias_url):
154- """Sends an error to the Launchpad errors list."""
155- template = get_email_template('notify-unhandled-email.txt')
156- # We add the error message in as a header too
157- # (X-Launchpad-Unhandled-Email) so we can create filters in the
158- # Launchpad-Error-Reports Mailman mailing list.
159- simple_sendmail(
160- get_bugmail_error_address(), [config.launchpad.errors_address],
161- 'Unhandled Email: %s' % file_alias_url,
162- template % {'url': file_alias_url, 'error_msg': message},
163- headers={'X-Launchpad-Unhandled-Email': message})
164-
165-
166-def generate_bug_add_email(bug, new_recipients=False, reason=None,
167- subscribed_by=None, event_creator=None):
168- """Generate a new bug notification from the given IBug.
169-
170- If new_recipients is supplied we generate a notification explaining
171- that the new recipients have been subscribed to the bug. Otherwise
172- it's just a notification of a new bug report.
173- """
174- subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
175- contents = ''
176-
177- if bug.private:
178- # This is a confidential bug.
179- visibility = u"Private"
180- else:
181- # This is a public bug.
182- visibility = u"Public"
183-
184- if bug.security_related:
185- visibility += ' security'
186- contents += '*** This bug is a security vulnerability ***\n\n'
187-
188- bug_info = []
189- # Add information about the affected upstreams and packages.
190- for bugtask in bug.bugtasks:
191- bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
192- bug_info.append(u" Importance: %s" % bugtask.importance.title)
193-
194- if bugtask.assignee:
195- # There's a person assigned to fix this task, so show that
196- # information too.
197- bug_info.append(
198- u" Assignee: %s" % bugtask.assignee.unique_displayname)
199- bug_info.append(u" Status: %s\n" % bugtask.status.title)
200-
201- if bug.tags:
202- bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
203-
204- mailwrapper = MailWrapper(width=72)
205- content_substitutions = {
206- 'visibility': visibility,
207- 'bug_url': canonical_url(bug),
208- 'bug_info': "\n".join(bug_info),
209- 'bug_title': bug.title,
210- 'description': mailwrapper.format(bug.description),
211- 'notification_rationale': reason,
212- }
213-
214- if new_recipients:
215- if "assignee" in reason:
216- contents += (
217- "You have been assigned a bug task for a %(visibility)s bug")
218- if event_creator is not None:
219- contents += " by %(assigner)s"
220- content_substitutions['assigner'] = (
221- event_creator.unique_displayname)
222- else:
223- contents += "You have been subscribed to a %(visibility)s bug"
224- if subscribed_by is not None:
225- contents += " by %(subscribed_by)s"
226- content_substitutions['subscribed_by'] = (
227- subscribed_by.unique_displayname)
228- contents += (":\n\n"
229- "%(description)s\n\n%(bug_info)s")
230- # The visibility appears mid-phrase so.. hack hack.
231- content_substitutions['visibility'] = visibility.lower()
232- # XXX: kiko, 2007-03-21:
233- # We should really have a centralized way of adding this
234- # footer, but right now we lack a INotificationRecipientSet
235- # for this particular situation.
236- contents += (
237- "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
238- else:
239- contents += ("%(visibility)s bug reported:\n\n"
240- "%(description)s\n\n%(bug_info)s")
241-
242- contents = contents % content_substitutions
243-
244- contents = contents.rstrip()
245-
246- return (subject, contents)
247-
248-
249 def get_unified_diff(old_text, new_text, text_width):
250 r"""Return a unified diff of the two texts.
251
252@@ -329,254 +147,6 @@
253 return text_diff
254
255
256-def _get_task_change_row(label, oldval_display, newval_display):
257- """Return a row formatted for display in task change info."""
258- return u"%(label)13s: %(oldval)s => %(newval)s\n" % {
259- 'label': label.capitalize(),
260- 'oldval': oldval_display,
261- 'newval': newval_display}
262-
263-
264-def _get_task_change_values(task_change, displayattrname):
265- """Return the old value and the new value for a task field change."""
266- oldval = task_change.get('old')
267- newval = task_change.get('new')
268-
269- oldval_display = None
270- newval_display = None
271-
272- if oldval:
273- oldval_display = getattr(oldval, displayattrname)
274- if newval:
275- newval_display = getattr(newval, displayattrname)
276-
277- return (oldval_display, newval_display)
278-
279-
280-def get_bug_delta(old_bug, new_bug, user):
281- """Compute the delta from old_bug to new_bug.
282-
283- old_bug and new_bug are IBug's. user is an IPerson. Returns an
284- IBugDelta if there are changes, or None if there were no changes.
285- """
286- changes = {}
287-
288- for field_name in ("title", "description", "name", "private",
289- "security_related", "duplicateof", "tags"):
290- # fields for which we show old => new when their values change
291- old_val = getattr(old_bug, field_name)
292- new_val = getattr(new_bug, field_name)
293- if old_val != new_val:
294- changes[field_name] = {}
295- changes[field_name]["old"] = old_val
296- changes[field_name]["new"] = new_val
297-
298- if changes:
299- changes["bug"] = new_bug
300- changes["bug_before_modification"] = old_bug
301- changes["bugurl"] = canonical_url(new_bug)
302- changes["user"] = user
303-
304- return BugDelta(**changes)
305- else:
306- return None
307-
308-
309-@block_implicit_flushes
310-def notify_bug_added(bug, event):
311- """Send an email notification that a bug was added.
312-
313- Event must be an IObjectCreatedEvent.
314- """
315-
316- bug.addCommentNotification(bug.initial_message)
317-
318-
319-@block_implicit_flushes
320-def notify_bug_modified(modified_bug, event):
321- """Notify the Cc'd list that this bug has been modified.
322-
323- modified_bug bug must be an IBug. event must be an
324- IObjectModifiedEvent.
325- """
326- bug_delta = get_bug_delta(
327- old_bug=event.object_before_modification,
328- new_bug=event.object, user=IPerson(event.user))
329-
330- if bug_delta is not None:
331- add_bug_change_notifications(bug_delta)
332-
333-
334-def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
335- """Return the indirect subscribers for a bug task.
336-
337- Return the list of people who should get notifications about
338- changes to the task because of having an indirect subscription
339- relationship with it (by subscribing to its target, being an
340- assignee or owner, etc...)
341-
342- If `recipients` is present, add the subscribers to the set of
343- bug notification recipients.
344- """
345- if bugtask.bug.private:
346- return set()
347-
348- also_notified_subscribers = set()
349-
350- # Assignees are indirect subscribers.
351- if bugtask.assignee:
352- also_notified_subscribers.add(bugtask.assignee)
353- if recipients is not None:
354- recipients.addAssignee(bugtask.assignee)
355-
356- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
357- also_notified_subscribers.update(
358- bugtask.target.getBugNotificationsRecipients(
359- recipients, level=level))
360-
361- if bugtask.milestone is not None:
362- also_notified_subscribers.update(
363- bugtask.milestone.getBugNotificationsRecipients(
364- recipients, level=level))
365-
366- # If the target's bug supervisor isn't set,
367- # we add the owner as a subscriber.
368- pillar = bugtask.pillar
369- if pillar.bug_supervisor is None:
370- also_notified_subscribers.add(pillar.owner)
371- if recipients is not None:
372- recipients.addRegistrant(pillar.owner, pillar)
373-
374- return sorted(
375- also_notified_subscribers,
376- key=operator.attrgetter('displayname'))
377-
378-
379-def add_bug_change_notifications(bug_delta, old_bugtask=None,
380- new_subscribers=None):
381- """Generate bug notifications and add them to the bug."""
382- changes = get_bug_changes(bug_delta)
383- recipients = bug_delta.bug.getBugNotificationRecipients(
384- old_bug=bug_delta.bug_before_modification,
385- level=BugNotificationLevel.METADATA)
386- if old_bugtask is not None:
387- old_bugtask_recipients = BugNotificationRecipients()
388- get_bugtask_indirect_subscribers(
389- old_bugtask, recipients=old_bugtask_recipients,
390- level=BugNotificationLevel.METADATA)
391- recipients.update(old_bugtask_recipients)
392- for change in changes:
393- # XXX 2009-03-17 gmb [bug=344125]
394- # This if..else should be removed once the new BugChange API
395- # is complete and ubiquitous.
396- if IBugChange.providedBy(change):
397- if isinstance(change, BugDuplicateChange):
398- no_dupe_master_recipients = (
399- bug_delta.bug.getBugNotificationRecipients(
400- old_bug=bug_delta.bug_before_modification,
401- level=BugNotificationLevel.METADATA,
402- include_master_dupe_subscribers=False))
403- bug_delta.bug.addChange(
404- change, recipients=no_dupe_master_recipients)
405- elif (isinstance(change, BugTaskAssigneeChange) and
406- new_subscribers is not None):
407- for person in new_subscribers:
408- reason, rationale = recipients.getReason(person)
409- if 'Assignee' in rationale:
410- recipients.remove(person)
411- bug_delta.bug.addChange(change, recipients=recipients)
412- else:
413- bug_delta.bug.addChange(change, recipients=recipients)
414- else:
415- bug_delta.bug.addChangeNotification(
416- change, person=bug_delta.user, recipients=recipients)
417-
418-
419-@block_implicit_flushes
420-def notify_bugtask_edited(modified_bugtask, event):
421- """Notify CC'd subscribers of this bug that something has changed
422- on this task.
423-
424- modified_bugtask must be an IBugTask. event must be an
425- IObjectModifiedEvent.
426- """
427- bugtask_delta = event.object.getDelta(event.object_before_modification)
428- bug_delta = BugDelta(
429- bug=event.object.bug,
430- bugurl=canonical_url(event.object.bug),
431- bugtask_deltas=bugtask_delta,
432- user=IPerson(event.user))
433-
434- event_creator = IPerson(event.user)
435- previous_subscribers = event.object_before_modification.bug_subscribers
436- current_subscribers = event.object.bug_subscribers
437- prev_subs_set = set(previous_subscribers)
438- cur_subs_set = set(current_subscribers)
439- new_subs = cur_subs_set.difference(prev_subs_set)
440-
441- add_bug_change_notifications(
442- bug_delta, old_bugtask=event.object_before_modification,
443- new_subscribers=new_subs)
444-
445- _send_bug_details_to_new_bug_subscribers(
446- event.object.bug, previous_subscribers, current_subscribers,
447- event_creator=event_creator)
448- update_security_contact_subscriptions(modified_bugtask, event)
449-
450-
451-@block_implicit_flushes
452-def notify_bug_comment_added(bugmessage, event):
453- """Notify CC'd list that a message was added to this bug.
454-
455- bugmessage must be an IBugMessage. event must be an
456- IObjectCreatedEvent. If bugmessage.bug is a duplicate the
457- comment will also be sent to the dup target's subscribers.
458- """
459- bug = bugmessage.bug
460- bug.addCommentNotification(bugmessage.message)
461-
462-
463-@block_implicit_flushes
464-def notify_bug_attachment_added(bugattachment, event):
465- """Notify CC'd list that a new attachment has been added.
466-
467- bugattachment must be an IBugAttachment. event must be an
468- IObjectCreatedEvent.
469- """
470- bug = bugattachment.bug
471- bug_delta = BugDelta(
472- bug=bug,
473- bugurl=canonical_url(bug),
474- user=IPerson(event.user),
475- attachment={'new': bugattachment, 'old': None})
476-
477- add_bug_change_notifications(bug_delta)
478-
479-
480-@block_implicit_flushes
481-def notify_bug_attachment_removed(bugattachment, event):
482- """Notify that an attachment has been removed."""
483- bug = bugattachment.bug
484- bug_delta = BugDelta(
485- bug=bug,
486- bugurl=canonical_url(bug),
487- user=IPerson(event.user),
488- attachment={'old': bugattachment, 'new': None})
489-
490- add_bug_change_notifications(bug_delta)
491-
492-
493-@block_implicit_flushes
494-def notify_bug_subscription_added(bug_subscription, event):
495- """Notify that a new bug subscription was added."""
496- # When a user is subscribed to a bug by someone other
497- # than themselves, we send them a notification email.
498- if bug_subscription.person != bug_subscription.subscribed_by:
499- _send_bug_details_to_new_bug_subscribers(
500- bug_subscription.bug, [], [bug_subscription.person],
501- subscribed_by=bug_subscription.subscribed_by)
502-
503-
504 @block_implicit_flushes
505 def notify_invitation_to_join_team(event):
506 """Notify team admins that the team has been invited to join another team.
507
508=== modified file 'lib/canonical/launchpad/subscribers/karma.py'
509--- lib/canonical/launchpad/subscribers/karma.py 2010-08-20 20:31:18 +0000
510+++ lib/canonical/launchpad/subscribers/karma.py 2010-08-23 20:18:06 +0000
511@@ -6,7 +6,7 @@
512
513 from canonical.database.sqlbase import block_implicit_flushes
514 from canonical.launchpad.interfaces import BugTaskStatus
515-from canonical.launchpad.mailnotification import get_bug_delta
516+from lp.bugs.subscribers.bug import get_bug_delta
517 from lp.code.enums import BranchMergeProposalStatus
518 from lp.registry.interfaces.person import IPerson
519
520@@ -18,6 +18,7 @@
521 assert len(bug.bugtasks) >= 1
522 _assignKarmaUsingBugContext(IPerson(event.user), bug, 'bugcreated')
523
524+
525 def _assign_karma_using_bugtask_context(person, bugtask, actionname):
526 """Extract the right context from the bugtask and assign karma."""
527 distribution = bugtask.distribution
528@@ -157,6 +158,7 @@
529 """Assign karma to the user who registered the branch."""
530 branch.target.assignKarma(branch.registrant, 'branchcreated')
531
532+
533 @block_implicit_flushes
534 def bug_branch_created(bug_branch, event):
535 """Assign karma to the user who linked the bug to the branch."""
536
537=== modified file 'lib/lp/bugs/configure.zcml'
538--- lib/lp/bugs/configure.zcml 2010-08-19 03:06:27 +0000
539+++ lib/lp/bugs/configure.zcml 2010-08-23 20:18:06 +0000
540@@ -55,25 +55,22 @@
541 handler="lp.bugs.subscribers.bugcreation.at_least_one_task"/>
542 <subscriber
543 for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectCreatedEvent"
544- handler="canonical.launchpad.mailnotification.notify_bug_added"/>
545+ handler="lp.bugs.subscribers.bug.notify_bug_added"/>
546 <subscriber
547 for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectCreatedEvent"
548 handler="canonical.launchpad.subscribers.karma.bug_created"/>
549 <subscriber
550 for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
551- handler="canonical.launchpad.mailnotification.notify_bug_modified"/>
552- <subscriber
553- for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
554 handler="canonical.launchpad.subscribers.karma.bug_modified"/>
555 <subscriber
556 for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
557 handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
558 <subscriber
559 for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectCreatedEvent"
560- handler="canonical.launchpad.mailnotification.notify_bug_attachment_added"/>
561+ handler="lp.bugs.subscribers.bug.notify_bug_attachment_added"/>
562 <subscriber
563 for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectDeletedEvent"
564- handler="canonical.launchpad.mailnotification.notify_bug_attachment_removed"/>
565+ handler="lp.bugs.subscribers.bug.notify_bug_attachment_removed"/>
566 <subscriber
567 for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectCreatedEvent"
568 handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
569@@ -94,7 +91,7 @@
570 handler="canonical.launchpad.subscribers.karma.cve_added"/>
571 <subscriber
572 for="canonical.launchpad.interfaces.IBugMessage lazr.lifecycle.interfaces.IObjectCreatedEvent"
573- handler="canonical.launchpad.mailnotification.notify_bug_comment_added"/>
574+ handler="lp.bugs.subscribers.bug.notify_bug_comment_added"/>
575 <subscriber
576 for="canonical.launchpad.interfaces.IBugMessage lazr.lifecycle.interfaces.IObjectCreatedEvent"
577 handler="canonical.launchpad.subscribers.karma.bug_comment_added"/>
578@@ -115,7 +112,7 @@
579 handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
580 <subscriber
581 for="canonical.launchpad.interfaces.IBugSubscription lazr.lifecycle.interfaces.IObjectCreatedEvent"
582- handler="canonical.launchpad.mailnotification.notify_bug_subscription_added"/>
583+ handler="lp.bugs.subscribers.bug.notify_bug_subscription_added"/>
584 <subscriber
585 for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
586 handler="lp.bugs.subscribers.bug.notify_bug_modified"/>
587@@ -932,7 +929,7 @@
588 handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
589 <subscriber
590 for="canonical.launchpad.interfaces.IBugTask lazr.lifecycle.interfaces.IObjectModifiedEvent"
591- handler="canonical.launchpad.mailnotification.notify_bugtask_edited"/>
592+ handler="lp.bugs.subscribers.bugtask.notify_bugtask_edited"/>
593 <subscriber
594 for="canonical.launchpad.interfaces.IBugTask lazr.lifecycle.interfaces.IObjectModifiedEvent"
595 handler="canonical.launchpad.subscribers.karma.bugtask_modified"/>
596
597=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
598--- lib/lp/bugs/doc/bugnotification-email.txt 2010-08-04 09:42:07 +0000
599+++ lib/lp/bugs/doc/bugnotification-email.txt 2010-08-23 20:18:06 +0000
600@@ -19,10 +19,8 @@
601 object it gets passed, the formatting logic has been cut into two
602 pieces: get_bug_changes and generate_bug_add_email.
603
604- >>> from lp.bugs.adapters.bugchange import (
605- ... get_bug_changes)
606- >>> from canonical.launchpad.mailnotification import (
607- ... generate_bug_add_email)
608+ >>> from lp.bugs.adapters.bugchange import get_bug_changes
609+ >>> from lp.bugs.mail.newbug import generate_bug_add_email
610
611 Let's demonstrate what the bugmails will look like, by going through
612 the various events that can happen that would cause a notification to
613@@ -479,8 +477,7 @@
614 mailnotification.py contains a class, BugNotificationBuilder, which is
615 used to construct bug notification emails.
616
617- >>> from canonical.launchpad.mailnotification import (
618- ... BugNotificationBuilder)
619+ >>> from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
620
621 When instantiatiated it derives a list of common unchanging headers
622 from the bug so that they are not calculated for every recipient.
623
624=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
625--- lib/lp/bugs/doc/bugsubscription.txt 2010-08-16 13:58:33 +0000
626+++ lib/lp/bugs/doc/bugsubscription.txt 2010-08-23 20:18:06 +0000
627@@ -101,10 +101,8 @@
628 It is also possible to get the list of indirect subscribers for an
629 individual bug task.
630
631- >>> from canonical.launchpad.mailnotification import (
632- ... get_bugtask_indirect_subscribers)
633- >>> get_bugtask_indirect_subscribers(
634- ... linux_source_bug.bugtasks[0])
635+ >>> from lp.bugs.subscribers.bug import get_bugtask_indirect_subscribers
636+ >>> get_bugtask_indirect_subscribers(linux_source_bug.bugtasks[0])
637 [<Person at ...>]
638
639 The list of all bug subscribers can also be accessed via
640
641=== added file 'lib/lp/bugs/mail/newbug.py'
642--- lib/lp/bugs/mail/newbug.py 1970-01-01 00:00:00 +0000
643+++ lib/lp/bugs/mail/newbug.py 2010-08-23 20:18:06 +0000
644@@ -0,0 +1,95 @@
645+# Copyright 2010 Canonical Ltd. This software is licensed under the
646+# GNU Affero General Public License version 3 (see the file LICENSE).
647+
648+"""Mail for new bugs."""
649+
650+__metaclass__ = type
651+__all__ = [
652+ 'generate_bug_add_email',
653+ ]
654+
655+from canonical.launchpad.webapp.publisher import canonical_url
656+from lp.services.mail.mailwrapper import MailWrapper
657+
658+
659+def generate_bug_add_email(bug, new_recipients=False, reason=None,
660+ subscribed_by=None, event_creator=None):
661+ """Generate a new bug notification from the given IBug.
662+
663+ If new_recipients is supplied we generate a notification explaining
664+ that the new recipients have been subscribed to the bug. Otherwise
665+ it's just a notification of a new bug report.
666+ """
667+ subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
668+ contents = ''
669+
670+ if bug.private:
671+ # This is a confidential bug.
672+ visibility = u"Private"
673+ else:
674+ # This is a public bug.
675+ visibility = u"Public"
676+
677+ if bug.security_related:
678+ visibility += ' security'
679+ contents += '*** This bug is a security vulnerability ***\n\n'
680+
681+ bug_info = []
682+ # Add information about the affected upstreams and packages.
683+ for bugtask in bug.bugtasks:
684+ bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
685+ bug_info.append(u" Importance: %s" % bugtask.importance.title)
686+
687+ if bugtask.assignee:
688+ # There's a person assigned to fix this task, so show that
689+ # information too.
690+ bug_info.append(
691+ u" Assignee: %s" % bugtask.assignee.unique_displayname)
692+ bug_info.append(u" Status: %s\n" % bugtask.status.title)
693+
694+ if bug.tags:
695+ bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
696+
697+ mailwrapper = MailWrapper(width=72)
698+ content_substitutions = {
699+ 'visibility': visibility,
700+ 'bug_url': canonical_url(bug),
701+ 'bug_info': "\n".join(bug_info),
702+ 'bug_title': bug.title,
703+ 'description': mailwrapper.format(bug.description),
704+ 'notification_rationale': reason,
705+ }
706+
707+ if new_recipients:
708+ if "assignee" in reason:
709+ contents += (
710+ "You have been assigned a bug task for a %(visibility)s bug")
711+ if event_creator is not None:
712+ contents += " by %(assigner)s"
713+ content_substitutions['assigner'] = (
714+ event_creator.unique_displayname)
715+ else:
716+ contents += "You have been subscribed to a %(visibility)s bug"
717+ if subscribed_by is not None:
718+ contents += " by %(subscribed_by)s"
719+ content_substitutions['subscribed_by'] = (
720+ subscribed_by.unique_displayname)
721+ contents += (":\n\n"
722+ "%(description)s\n\n%(bug_info)s")
723+ # The visibility appears mid-phrase so.. hack hack.
724+ content_substitutions['visibility'] = visibility.lower()
725+ # XXX: kiko, 2007-03-21:
726+ # We should really have a centralized way of adding this
727+ # footer, but right now we lack a INotificationRecipientSet
728+ # for this particular situation.
729+ contents += (
730+ "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
731+ else:
732+ contents += ("%(visibility)s bug reported:\n\n"
733+ "%(description)s\n\n%(bug_info)s")
734+
735+ contents = contents % content_substitutions
736+
737+ contents = contents.rstrip()
738+
739+ return (subject, contents)
740
741=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
742--- lib/lp/bugs/scripts/bugnotification.py 2010-08-20 20:31:18 +0000
743+++ lib/lp/bugs/scripts/bugnotification.py 2010-08-23 20:18:06 +0000
744@@ -27,10 +27,6 @@
745 get_email_template,
746 )
747 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
748-from canonical.launchpad.mailnotification import (
749- generate_bug_add_email,
750- MailWrapper,
751- )
752 from canonical.launchpad.scripts.logger import log
753 from canonical.launchpad.webapp import canonical_url
754 from lp.bugs.interfaces.bugmessage import IBugMessageSet
755@@ -38,7 +34,9 @@
756 BugNotificationBuilder,
757 get_bugmail_from_address,
758 )
759+from lp.bugs.mail.newbug import generate_bug_add_email
760 from lp.registry.interfaces.person import IPersonSet
761+from lp.services.mail.mailwrapper import MailWrapper
762
763
764 def construct_email_notifications(bug_notifications):
765
766=== modified file 'lib/lp/bugs/subscribers/bug.py'
767--- lib/lp/bugs/subscribers/bug.py 2010-01-08 03:12:30 +0000
768+++ lib/lp/bugs/subscribers/bug.py 2010-08-23 20:18:06 +0000
769@@ -2,19 +2,63 @@
770 # GNU Affero General Public License version 3 (see the file LICENSE).
771
772 __metaclass__ = type
773-__all__ = ['notify_bug_modified']
774-
775-
776+__all__ = [
777+ 'add_bug_change_notifications',
778+ 'get_bug_delta',
779+ 'get_bugtask_indirect_subscribers',
780+ 'notify_bug_added',
781+ 'notify_bug_attachment_added',
782+ 'notify_bug_attachment_removed',
783+ 'notify_bug_comment_added',
784+ 'notify_bug_modified',
785+ 'notify_bug_subscription_added',
786+ 'send_bug_details_to_new_bug_subscribers',
787+ ]
788+
789+
790+import datetime
791+from operator import attrgetter
792+
793+from canonical.config import config
794 from canonical.database.sqlbase import block_implicit_flushes
795+from canonical.launchpad.helpers import get_contact_email_addresses
796+from canonical.launchpad.mail import (
797+ format_address,
798+ sendmail,
799+ )
800+from canonical.launchpad.webapp.publisher import canonical_url
801+from lp.bugs.adapters.bugchange import (
802+ BugDuplicateChange,
803+ BugTaskAssigneeChange,
804+ get_bug_changes,
805+ )
806+from lp.bugs.adapters.bugdelta import BugDelta
807+from lp.bugs.interfaces.bugchange import IBugChange
808+from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
809+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
810+from lp.bugs.mail.newbug import generate_bug_add_email
811+from lp.registry.enum import BugNotificationLevel
812 from lp.registry.interfaces.person import IPerson
813+from lp.registry.interfaces.structuralsubscription import (
814+ IStructuralSubscriptionTarget,
815+ )
816+
817+
818+@block_implicit_flushes
819+def notify_bug_added(bug, event):
820+ """Send an email notification that a bug was added.
821+
822+ Event must be an IObjectCreatedEvent.
823+ """
824+ bug.addCommentNotification(bug.initial_message)
825
826
827 @block_implicit_flushes
828 def notify_bug_modified(bug, event):
829 """Handle bug change events.
830
831- Subscribe the security contacts for a bug when it
832- becomes security-related.
833+ Subscribe the security contacts for a bug when it becomes
834+ security-related, and add notifications for the changes.
835 """
836 if (event.object.security_related and
837 not event.object_before_modification.security_related):
838@@ -23,3 +67,222 @@
839 for pillar in bug.affected_pillars:
840 if pillar.security_contact is not None:
841 bug.subscribe(pillar.security_contact, IPerson(event.user))
842+
843+ bug_delta = get_bug_delta(
844+ old_bug=event.object_before_modification,
845+ new_bug=event.object, user=IPerson(event.user))
846+
847+ if bug_delta is not None:
848+ add_bug_change_notifications(bug_delta)
849+
850+
851+@block_implicit_flushes
852+def notify_bug_comment_added(bugmessage, event):
853+ """Notify CC'd list that a message was added to this bug.
854+
855+ bugmessage must be an IBugMessage. event must be an
856+ IObjectCreatedEvent. If bugmessage.bug is a duplicate the
857+ comment will also be sent to the dup target's subscribers.
858+ """
859+ bug = bugmessage.bug
860+ bug.addCommentNotification(bugmessage.message)
861+
862+
863+@block_implicit_flushes
864+def notify_bug_attachment_added(bugattachment, event):
865+ """Notify CC'd list that a new attachment has been added.
866+
867+ bugattachment must be an IBugAttachment. event must be an
868+ IObjectCreatedEvent.
869+ """
870+ bug = bugattachment.bug
871+ bug_delta = BugDelta(
872+ bug=bug,
873+ bugurl=canonical_url(bug),
874+ user=IPerson(event.user),
875+ attachment={'new': bugattachment, 'old': None})
876+
877+ add_bug_change_notifications(bug_delta)
878+
879+
880+@block_implicit_flushes
881+def notify_bug_attachment_removed(bugattachment, event):
882+ """Notify that an attachment has been removed."""
883+ bug = bugattachment.bug
884+ bug_delta = BugDelta(
885+ bug=bug,
886+ bugurl=canonical_url(bug),
887+ user=IPerson(event.user),
888+ attachment={'old': bugattachment, 'new': None})
889+
890+ add_bug_change_notifications(bug_delta)
891+
892+
893+@block_implicit_flushes
894+def notify_bug_subscription_added(bug_subscription, event):
895+ """Notify that a new bug subscription was added."""
896+ # When a user is subscribed to a bug by someone other
897+ # than themselves, we send them a notification email.
898+ if bug_subscription.person != bug_subscription.subscribed_by:
899+ send_bug_details_to_new_bug_subscribers(
900+ bug_subscription.bug, [], [bug_subscription.person],
901+ subscribed_by=bug_subscription.subscribed_by)
902+
903+
904+def get_bug_delta(old_bug, new_bug, user):
905+ """Compute the delta from old_bug to new_bug.
906+
907+ old_bug and new_bug are IBug's. user is an IPerson. Returns an
908+ IBugDelta if there are changes, or None if there were no changes.
909+ """
910+ changes = {}
911+
912+ for field_name in ("title", "description", "name", "private",
913+ "security_related", "duplicateof", "tags"):
914+ # fields for which we show old => new when their values change
915+ old_val = getattr(old_bug, field_name)
916+ new_val = getattr(new_bug, field_name)
917+ if old_val != new_val:
918+ changes[field_name] = {}
919+ changes[field_name]["old"] = old_val
920+ changes[field_name]["new"] = new_val
921+
922+ if changes:
923+ changes["bug"] = new_bug
924+ changes["bug_before_modification"] = old_bug
925+ changes["bugurl"] = canonical_url(new_bug)
926+ changes["user"] = user
927+ return BugDelta(**changes)
928+ else:
929+ return None
930+
931+
932+def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
933+ """Return the indirect subscribers for a bug task.
934+
935+ Return the list of people who should get notifications about
936+ changes to the task because of having an indirect subscription
937+ relationship with it (by subscribing to its target, being an
938+ assignee or owner, etc...)
939+
940+ If `recipients` is present, add the subscribers to the set of
941+ bug notification recipients.
942+ """
943+ if bugtask.bug.private:
944+ return set()
945+
946+ also_notified_subscribers = set()
947+
948+ # Assignees are indirect subscribers.
949+ if bugtask.assignee:
950+ also_notified_subscribers.add(bugtask.assignee)
951+ if recipients is not None:
952+ recipients.addAssignee(bugtask.assignee)
953+
954+ if IStructuralSubscriptionTarget.providedBy(bugtask.target):
955+ also_notified_subscribers.update(
956+ bugtask.target.getBugNotificationsRecipients(
957+ recipients, level=level))
958+
959+ if bugtask.milestone is not None:
960+ also_notified_subscribers.update(
961+ bugtask.milestone.getBugNotificationsRecipients(
962+ recipients, level=level))
963+
964+ # If the target's bug supervisor isn't set,
965+ # we add the owner as a subscriber.
966+ pillar = bugtask.pillar
967+ if pillar.bug_supervisor is None:
968+ also_notified_subscribers.add(pillar.owner)
969+ if recipients is not None:
970+ recipients.addRegistrant(pillar.owner, pillar)
971+
972+ return sorted(
973+ also_notified_subscribers,
974+ key=attrgetter('displayname'))
975+
976+
977+def add_bug_change_notifications(bug_delta, old_bugtask=None,
978+ new_subscribers=None):
979+ """Generate bug notifications and add them to the bug."""
980+ changes = get_bug_changes(bug_delta)
981+ recipients = bug_delta.bug.getBugNotificationRecipients(
982+ old_bug=bug_delta.bug_before_modification,
983+ level=BugNotificationLevel.METADATA)
984+ if old_bugtask is not None:
985+ old_bugtask_recipients = BugNotificationRecipients()
986+ get_bugtask_indirect_subscribers(
987+ old_bugtask, recipients=old_bugtask_recipients,
988+ level=BugNotificationLevel.METADATA)
989+ recipients.update(old_bugtask_recipients)
990+ for change in changes:
991+ # XXX 2009-03-17 gmb [bug=344125]
992+ # This if..else should be removed once the new BugChange API
993+ # is complete and ubiquitous.
994+ if IBugChange.providedBy(change):
995+ if isinstance(change, BugDuplicateChange):
996+ no_dupe_master_recipients = (
997+ bug_delta.bug.getBugNotificationRecipients(
998+ old_bug=bug_delta.bug_before_modification,
999+ level=BugNotificationLevel.METADATA,
1000+ include_master_dupe_subscribers=False))
1001+ bug_delta.bug.addChange(
1002+ change, recipients=no_dupe_master_recipients)
1003+ elif (isinstance(change, BugTaskAssigneeChange) and
1004+ new_subscribers is not None):
1005+ for person in new_subscribers:
1006+ reason, rationale = recipients.getReason(person)
1007+ if 'Assignee' in rationale:
1008+ recipients.remove(person)
1009+ bug_delta.bug.addChange(change, recipients=recipients)
1010+ else:
1011+ bug_delta.bug.addChange(change, recipients=recipients)
1012+ else:
1013+ bug_delta.bug.addChangeNotification(
1014+ change, person=bug_delta.user, recipients=recipients)
1015+
1016+
1017+def send_bug_details_to_new_bug_subscribers(
1018+ bug, previous_subscribers, current_subscribers, subscribed_by=None,
1019+ event_creator=None):
1020+ """Send an email containing full bug details to new bug subscribers.
1021+
1022+ This function is designed to handle situations where bugtasks get
1023+ reassigned to new products or sourcepackages, and the new bug subscribers
1024+ need to be notified of the bug.
1025+ """
1026+ prev_subs_set = set(previous_subscribers)
1027+ cur_subs_set = set(current_subscribers)
1028+ new_subs = cur_subs_set.difference(prev_subs_set)
1029+
1030+ to_addrs = set()
1031+ for new_sub in new_subs:
1032+ to_addrs.update(get_contact_email_addresses(new_sub))
1033+
1034+ if not to_addrs:
1035+ return
1036+
1037+ from_addr = format_address(
1038+ 'Launchpad Bug Tracker',
1039+ "%s@%s" % (bug.id, config.launchpad.bugs_domain))
1040+ # Now's a good a time as any for this email; don't use the original
1041+ # reported date for the bug as it will just confuse mailer and
1042+ # recipient.
1043+ email_date = datetime.datetime.now()
1044+
1045+ # The new subscriber email is effectively the initial message regarding
1046+ # a new bug. The bug's initial message is used in the References
1047+ # header to establish the message's context in the email client.
1048+ references = [bug.initial_message.rfc822msgid]
1049+ recipients = bug.getBugNotificationRecipients()
1050+
1051+ bug_notification_builder = BugNotificationBuilder(bug, event_creator)
1052+ for to_addr in sorted(to_addrs):
1053+ reason, rationale = recipients.getReason(to_addr)
1054+ subject, contents = generate_bug_add_email(
1055+ bug, new_recipients=True, subscribed_by=subscribed_by,
1056+ reason=reason, event_creator=event_creator)
1057+ msg = bug_notification_builder.build(
1058+ from_addr, to_addr, contents, subject, email_date,
1059+ rationale=rationale, references=references)
1060+ sendmail(msg)
1061
1062=== modified file 'lib/lp/bugs/subscribers/bugcreation.py'
1063--- lib/lp/bugs/subscribers/bugcreation.py 2010-08-20 20:31:18 +0000
1064+++ lib/lp/bugs/subscribers/bugcreation.py 2010-08-23 20:18:06 +0000
1065@@ -2,8 +2,10 @@
1066 # GNU Affero General Public License version 3 (see the file LICENSE).
1067
1068 __metaclass__ = type
1069+__all__ = [
1070+ 'at_least_one_task',
1071+ ]
1072
1073-from canonical.database.sqlbase import block_implicit_flushes
1074 from lp.bugs.interfaces.bug import CreatedBugWithNoBugTasksError
1075
1076
1077
1078=== added file 'lib/lp/bugs/subscribers/bugtask.py'
1079--- lib/lp/bugs/subscribers/bugtask.py 1970-01-01 00:00:00 +0000
1080+++ lib/lp/bugs/subscribers/bugtask.py 2010-08-23 20:18:06 +0000
1081@@ -0,0 +1,79 @@
1082+# Copyright 2010 Canonical Ltd. This software is licensed under the
1083+# GNU Affero General Public License version 3 (see the file LICENSE).
1084+
1085+__metaclass__ = type
1086+__all__ = [
1087+ 'notify_bugtask_edited',
1088+ 'update_security_contact_subscriptions',
1089+ ]
1090+
1091+
1092+from canonical.database.sqlbase import block_implicit_flushes
1093+from canonical.launchpad.webapp.publisher import canonical_url
1094+from lp.bugs.adapters.bugdelta import BugDelta
1095+from lp.bugs.interfaces.bugtask import IUpstreamBugTask
1096+from lp.bugs.subscribers.bug import (
1097+ add_bug_change_notifications,
1098+ send_bug_details_to_new_bug_subscribers,
1099+ )
1100+from lp.registry.interfaces.person import IPerson
1101+
1102+
1103+@block_implicit_flushes
1104+def update_security_contact_subscriptions(event):
1105+ """Subscribe the new security contact when a bugtask's product changes.
1106+
1107+ Only subscribes the new security contact if the bug was marked a
1108+ security issue originally.
1109+
1110+ No change is made for private bugs.
1111+ """
1112+ if event.object.bug.private:
1113+ return
1114+
1115+ if not IUpstreamBugTask.providedBy(event.object):
1116+ return
1117+
1118+ bugtask_before_modification = event.object_before_modification
1119+ bugtask_after_modification = event.object
1120+
1121+ if (bugtask_before_modification.product !=
1122+ bugtask_after_modification.product):
1123+ new_product = bugtask_after_modification.product
1124+ if (bugtask_before_modification.bug.security_related and
1125+ new_product.security_contact):
1126+ bugtask_after_modification.bug.subscribe(
1127+ new_product.security_contact, IPerson(event.user))
1128+
1129+
1130+@block_implicit_flushes
1131+def notify_bugtask_edited(modified_bugtask, event):
1132+ """Notify CC'd subscribers of this bug that something has changed
1133+ on this task.
1134+
1135+ modified_bugtask must be an IBugTask. event must be an
1136+ IObjectModifiedEvent.
1137+ """
1138+ bugtask_delta = event.object.getDelta(event.object_before_modification)
1139+ bug_delta = BugDelta(
1140+ bug=event.object.bug,
1141+ bugurl=canonical_url(event.object.bug),
1142+ bugtask_deltas=bugtask_delta,
1143+ user=IPerson(event.user))
1144+
1145+ event_creator = IPerson(event.user)
1146+ previous_subscribers = event.object_before_modification.bug_subscribers
1147+ current_subscribers = event.object.bug_subscribers
1148+ prev_subs_set = set(previous_subscribers)
1149+ cur_subs_set = set(current_subscribers)
1150+ new_subs = cur_subs_set.difference(prev_subs_set)
1151+
1152+ add_bug_change_notifications(
1153+ bug_delta, old_bugtask=event.object_before_modification,
1154+ new_subscribers=new_subs)
1155+
1156+ send_bug_details_to_new_bug_subscribers(
1157+ event.object.bug, previous_subscribers, current_subscribers,
1158+ event_creator=event_creator)
1159+
1160+ update_security_contact_subscriptions(event)