launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00646
[Merge] lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel
Gavin Panella has proposed merging lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel with lp:~allenap/launchpad/refactor-get-email-notifications as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Moves almost all of the remaining bug related code out of c/l/mailnotification.py. Also removes a few unused functions.
--
https://code.launchpad.net/~allenap/launchpad/refactor-mailnotification/+merge/32923
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/refactor-mailnotification into lp:launchpad/devel.
=== removed file 'lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt'
--- lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt 2005-10-31 18:29:12 +0000
+++ lib/canonical/launchpad/emailtemplates/notify-unhandled-email.txt 1970-01-01 00:00:00 +0000
@@ -1,7 +0,0 @@
-The following email was unhandled:
-
-%(url)s
-
-Error message:
-
-%(error_msg)s
=== modified file 'lib/canonical/launchpad/mailnotification.py'
--- lib/canonical/launchpad/mailnotification.py 2010-07-28 22:33:59 +0000
+++ lib/canonical/launchpad/mailnotification.py 2010-08-17 20:02:42 +0000
@@ -8,18 +8,15 @@
__metaclass__ = type
-import datetime
+import re
+
from difflib import unified_diff
-import operator
-
from email.Header import Header
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMEMessage import MIMEMessage
from email.Utils import formataddr, make_msgid
-import re
-
from zope.component import getAdapter, getUtility
from canonical.config import config
@@ -28,111 +25,30 @@
get_contact_email_addresses, get_email_template)
from canonical.launchpad.interfaces import (
IHeldMessageDetails, IPerson, IPersonSet, ISpecification,
- IStructuralSubscriptionTarget, ITeamMembershipSet, IUpstreamBugTask,
- TeamMembershipStatus)
+ ITeamMembershipSet, TeamMembershipStatus)
from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
from canonical.launchpad.interfaces.message import (
IDirectEmailAuthorization, QuotaReachedError)
from canonical.launchpad.mail import (
- sendmail, simple_sendmail, simple_sendmail_from_person, format_address)
+ format_address, sendmail, simple_sendmail, simple_sendmail_from_person)
from canonical.launchpad.webapp.publisher import canonical_url
from canonical.launchpad.webapp.url import urlappend
-from lp.bugs.adapters.bugdelta import BugDelta
-from lp.bugs.adapters.bugchange import (
- BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange)
-from lp.bugs.interfaces.bugchange import IBugChange
from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address
-from lp.registry.interfaces.structuralsubscription import (
- BugNotificationLevel)
from lp.services.mail.mailwrapper import MailWrapper
# XXX 2010-06-16 gmb bug=594985
# This shouldn't be here, but if we take it out lots of things cry,
# which is sad.
-from lp.services.mail.notificationrecipientset import (
- NotificationRecipientSet)
-
-from lp.bugs.mail.bugnotificationbuilder import (
- BugNotificationBuilder)
-from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
+from lp.services.mail.notificationrecipientset import NotificationRecipientSet
+
+# Silence lint warnings.
+NotificationRecipientSet
+
CC = "CC"
-def _send_bug_details_to_new_bug_subscribers(
- bug, previous_subscribers, current_subscribers, subscribed_by=None,
- event_creator=None):
- """Send an email containing full bug details to new bug subscribers.
-
- This function is designed to handle situations where bugtasks get
- reassigned to new products or sourcepackages, and the new bug subscribers
- need to be notified of the bug.
- """
- prev_subs_set = set(previous_subscribers)
- cur_subs_set = set(current_subscribers)
- new_subs = cur_subs_set.difference(prev_subs_set)
-
- to_addrs = set()
- for new_sub in new_subs:
- to_addrs.update(get_contact_email_addresses(new_sub))
-
- if not to_addrs:
- return
-
- from_addr = format_address(
- 'Launchpad Bug Tracker',
- "%s@%s" % (bug.id, config.launchpad.bugs_domain))
- # Now's a good a time as any for this email; don't use the original
- # reported date for the bug as it will just confuse mailer and
- # recipient.
- email_date = datetime.datetime.now()
-
- # The new subscriber email is effectively the initial message regarding
- # a new bug. The bug's initial message is used in the References
- # header to establish the message's context in the email client.
- references = [bug.initial_message.rfc822msgid]
- recipients = bug.getBugNotificationRecipients()
-
- bug_notification_builder = BugNotificationBuilder(bug, event_creator)
- for to_addr in sorted(to_addrs):
- reason, rationale = recipients.getReason(to_addr)
- subject, contents = generate_bug_add_email(
- bug, new_recipients=True, subscribed_by=subscribed_by,
- reason=reason, event_creator=event_creator)
- msg = bug_notification_builder.build(
- from_addr, to_addr, contents, subject, email_date,
- rationale=rationale, references=references)
- sendmail(msg)
-
-
-@block_implicit_flushes
-def update_security_contact_subscriptions(modified_bugtask, event):
- """Subscribe the new security contact when a bugtask's product changes.
-
- Only subscribes the new security contact if the bug was marked a
- security issue originally.
-
- No change is made for private bugs.
- """
- if event.object.bug.private:
- return
-
- if not IUpstreamBugTask.providedBy(event.object):
- return
-
- bugtask_before_modification = event.object_before_modification
- bugtask_after_modification = event.object
-
- if (bugtask_before_modification.product !=
- bugtask_after_modification.product):
- new_product = bugtask_after_modification.product
- if (bugtask_before_modification.bug.security_related and
- new_product.security_contact):
- bugtask_after_modification.bug.subscribe(
- new_product.security_contact, IPerson(event.user))
-
-
def send_process_error_notification(to_address, subject, error_msg,
original_msg, failing_command=None):
"""Send a mail about an error occurring while using the email interface.
@@ -175,100 +91,6 @@
sendmail(msg)
-def notify_errors_list(message, file_alias_url):
- """Sends an error to the Launchpad errors list."""
- template = get_email_template('notify-unhandled-email.txt')
- # We add the error message in as a header too
- # (X-Launchpad-Unhandled-Email) so we can create filters in the
- # Launchpad-Error-Reports Mailman mailing list.
- simple_sendmail(
- get_bugmail_error_address(), [config.launchpad.errors_address],
- 'Unhandled Email: %s' % file_alias_url,
- template % {'url': file_alias_url, 'error_msg': message},
- headers={'X-Launchpad-Unhandled-Email': message})
-
-def generate_bug_add_email(bug, new_recipients=False, reason=None,
- subscribed_by=None, event_creator=None):
- """Generate a new bug notification from the given IBug.
-
- If new_recipients is supplied we generate a notification explaining
- that the new recipients have been subscribed to the bug. Otherwise
- it's just a notification of a new bug report.
- """
- subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
- contents = ''
-
- if bug.private:
- # This is a confidential bug.
- visibility = u"Private"
- else:
- # This is a public bug.
- visibility = u"Public"
-
- if bug.security_related:
- visibility += ' security'
- contents += '*** This bug is a security vulnerability ***\n\n'
-
- bug_info = []
- # Add information about the affected upstreams and packages.
- for bugtask in bug.bugtasks:
- bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
- bug_info.append(u" Importance: %s" % bugtask.importance.title)
-
- if bugtask.assignee:
- # There's a person assigned to fix this task, so show that
- # information too.
- bug_info.append(
- u" Assignee: %s" % bugtask.assignee.unique_displayname)
- bug_info.append(u" Status: %s\n" % bugtask.status.title)
-
- if bug.tags:
- bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
-
- mailwrapper = MailWrapper(width=72)
- content_substitutions = {
- 'visibility': visibility,
- 'bug_url': canonical_url(bug),
- 'bug_info': "\n".join(bug_info),
- 'bug_title': bug.title,
- 'description': mailwrapper.format(bug.description),
- 'notification_rationale': reason,
- }
-
- if new_recipients:
- if "assignee" in reason:
- contents += "You have been assigned a bug task for a %(visibility)s bug"
- if event_creator is not None:
- contents += " by %(assigner)s"
- content_substitutions['assigner'] = (
- event_creator.unique_displayname)
- else:
- contents += "You have been subscribed to a %(visibility)s bug"
- if subscribed_by is not None:
- contents += " by %(subscribed_by)s"
- content_substitutions['subscribed_by'] = (
- subscribed_by.unique_displayname)
- contents += (":\n\n"
- "%(description)s\n\n%(bug_info)s")
- # The visibility appears mid-phrase so.. hack hack.
- content_substitutions['visibility'] = visibility.lower()
- # XXX: kiko, 2007-03-21:
- # We should really have a centralized way of adding this
- # footer, but right now we lack a INotificationRecipientSet
- # for this particular situation.
- contents += (
- "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
- else:
- contents += ("%(visibility)s bug reported:\n\n"
- "%(description)s\n\n%(bug_info)s")
-
- contents = contents % content_substitutions
-
- contents = contents.rstrip()
-
- return (subject, contents)
-
-
def get_unified_diff(old_text, new_text, text_width):
r"""Return a unified diff of the two texts.
@@ -309,254 +131,6 @@
return text_diff
-def _get_task_change_row(label, oldval_display, newval_display):
- """Return a row formatted for display in task change info."""
- return u"%(label)13s: %(oldval)s => %(newval)s\n" % {
- 'label': label.capitalize(),
- 'oldval': oldval_display,
- 'newval': newval_display}
-
-
-def _get_task_change_values(task_change, displayattrname):
- """Return the old value and the new value for a task field change."""
- oldval = task_change.get('old')
- newval = task_change.get('new')
-
- oldval_display = None
- newval_display = None
-
- if oldval:
- oldval_display = getattr(oldval, displayattrname)
- if newval:
- newval_display = getattr(newval, displayattrname)
-
- return (oldval_display, newval_display)
-
-
-def get_bug_delta(old_bug, new_bug, user):
- """Compute the delta from old_bug to new_bug.
-
- old_bug and new_bug are IBug's. user is an IPerson. Returns an
- IBugDelta if there are changes, or None if there were no changes.
- """
- changes = {}
-
- for field_name in ("title", "description", "name", "private",
- "security_related", "duplicateof", "tags"):
- # fields for which we show old => new when their values change
- old_val = getattr(old_bug, field_name)
- new_val = getattr(new_bug, field_name)
- if old_val != new_val:
- changes[field_name] = {}
- changes[field_name]["old"] = old_val
- changes[field_name]["new"] = new_val
-
- if changes:
- changes["bug"] = new_bug
- changes["bug_before_modification"] = old_bug
- changes["bugurl"] = canonical_url(new_bug)
- changes["user"] = user
-
- return BugDelta(**changes)
- else:
- return None
-
-
-@block_implicit_flushes
-def notify_bug_added(bug, event):
- """Send an email notification that a bug was added.
-
- Event must be an IObjectCreatedEvent.
- """
-
- bug.addCommentNotification(bug.initial_message)
-
-
-@block_implicit_flushes
-def notify_bug_modified(modified_bug, event):
- """Notify the Cc'd list that this bug has been modified.
-
- modified_bug bug must be an IBug. event must be an
- IObjectModifiedEvent.
- """
- bug_delta = get_bug_delta(
- old_bug=event.object_before_modification,
- new_bug=event.object, user=IPerson(event.user))
-
- if bug_delta is not None:
- add_bug_change_notifications(bug_delta)
-
-
-def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
- """Return the indirect subscribers for a bug task.
-
- Return the list of people who should get notifications about
- changes to the task because of having an indirect subscription
- relationship with it (by subscribing to its target, being an
- assignee or owner, etc...)
-
- If `recipients` is present, add the subscribers to the set of
- bug notification recipients.
- """
- if bugtask.bug.private:
- return set()
-
- also_notified_subscribers = set()
-
- # Assignees are indirect subscribers.
- if bugtask.assignee:
- also_notified_subscribers.add(bugtask.assignee)
- if recipients is not None:
- recipients.addAssignee(bugtask.assignee)
-
- if IStructuralSubscriptionTarget.providedBy(bugtask.target):
- also_notified_subscribers.update(
- bugtask.target.getBugNotificationsRecipients(
- recipients, level=level))
-
- if bugtask.milestone is not None:
- also_notified_subscribers.update(
- bugtask.milestone.getBugNotificationsRecipients(
- recipients, level=level))
-
- # If the target's bug supervisor isn't set,
- # we add the owner as a subscriber.
- pillar = bugtask.pillar
- if pillar.bug_supervisor is None:
- also_notified_subscribers.add(pillar.owner)
- if recipients is not None:
- recipients.addRegistrant(pillar.owner, pillar)
-
- return sorted(
- also_notified_subscribers,
- key=operator.attrgetter('displayname'))
-
-
-def add_bug_change_notifications(bug_delta, old_bugtask=None,
- new_subscribers=None):
- """Generate bug notifications and add them to the bug."""
- changes = get_bug_changes(bug_delta)
- recipients = bug_delta.bug.getBugNotificationRecipients(
- old_bug=bug_delta.bug_before_modification,
- level=BugNotificationLevel.METADATA)
- if old_bugtask is not None:
- old_bugtask_recipients = BugNotificationRecipients()
- get_bugtask_indirect_subscribers(
- old_bugtask, recipients=old_bugtask_recipients,
- level=BugNotificationLevel.METADATA)
- recipients.update(old_bugtask_recipients)
- for change in changes:
- # XXX 2009-03-17 gmb [bug=344125]
- # This if..else should be removed once the new BugChange API
- # is complete and ubiquitous.
- if IBugChange.providedBy(change):
- if isinstance(change, BugDuplicateChange):
- no_dupe_master_recipients = (
- bug_delta.bug.getBugNotificationRecipients(
- old_bug=bug_delta.bug_before_modification,
- level=BugNotificationLevel.METADATA,
- include_master_dupe_subscribers=False))
- bug_delta.bug.addChange(
- change, recipients=no_dupe_master_recipients)
- elif (isinstance(change, BugTaskAssigneeChange) and
- new_subscribers is not None):
- for person in new_subscribers:
- reason, rationale = recipients.getReason(person)
- if 'Assignee' in rationale:
- recipients.remove(person)
- bug_delta.bug.addChange(change, recipients=recipients)
- else:
- bug_delta.bug.addChange(change, recipients=recipients)
- else:
- bug_delta.bug.addChangeNotification(
- change, person=bug_delta.user, recipients=recipients)
-
-
-@block_implicit_flushes
-def notify_bugtask_edited(modified_bugtask, event):
- """Notify CC'd subscribers of this bug that something has changed
- on this task.
-
- modified_bugtask must be an IBugTask. event must be an
- IObjectModifiedEvent.
- """
- bugtask_delta = event.object.getDelta(event.object_before_modification)
- bug_delta = BugDelta(
- bug=event.object.bug,
- bugurl=canonical_url(event.object.bug),
- bugtask_deltas=bugtask_delta,
- user=IPerson(event.user))
-
- event_creator = IPerson(event.user)
- previous_subscribers = event.object_before_modification.bug_subscribers
- current_subscribers = event.object.bug_subscribers
- prev_subs_set = set(previous_subscribers)
- cur_subs_set = set(current_subscribers)
- new_subs = cur_subs_set.difference(prev_subs_set)
-
- add_bug_change_notifications(
- bug_delta, old_bugtask=event.object_before_modification,
- new_subscribers=new_subs)
-
- _send_bug_details_to_new_bug_subscribers(
- event.object.bug, previous_subscribers, current_subscribers,
- event_creator=event_creator)
- update_security_contact_subscriptions(modified_bugtask, event)
-
-
-@block_implicit_flushes
-def notify_bug_comment_added(bugmessage, event):
- """Notify CC'd list that a message was added to this bug.
-
- bugmessage must be an IBugMessage. event must be an
- IObjectCreatedEvent. If bugmessage.bug is a duplicate the
- comment will also be sent to the dup target's subscribers.
- """
- bug = bugmessage.bug
- bug.addCommentNotification(bugmessage.message)
-
-
-@block_implicit_flushes
-def notify_bug_attachment_added(bugattachment, event):
- """Notify CC'd list that a new attachment has been added.
-
- bugattachment must be an IBugAttachment. event must be an
- IObjectCreatedEvent.
- """
- bug = bugattachment.bug
- bug_delta = BugDelta(
- bug=bug,
- bugurl=canonical_url(bug),
- user=IPerson(event.user),
- attachment={'new': bugattachment, 'old': None})
-
- add_bug_change_notifications(bug_delta)
-
-
-@block_implicit_flushes
-def notify_bug_attachment_removed(bugattachment, event):
- """Notify that an attachment has been removed."""
- bug = bugattachment.bug
- bug_delta = BugDelta(
- bug=bug,
- bugurl=canonical_url(bug),
- user=IPerson(event.user),
- attachment={'old': bugattachment, 'new': None})
-
- add_bug_change_notifications(bug_delta)
-
-
-@block_implicit_flushes
-def notify_bug_subscription_added(bug_subscription, event):
- """Notify that a new bug subscription was added."""
- # When a user is subscribed to a bug by someone other
- # than themselves, we send them a notification email.
- if bug_subscription.person != bug_subscription.subscribed_by:
- _send_bug_details_to_new_bug_subscribers(
- bug_subscription.bug, [], [bug_subscription.person],
- subscribed_by=bug_subscription.subscribed_by)
-
-
@block_implicit_flushes
def notify_invitation_to_join_team(event):
"""Notify team admins that the team has been invited to join another team.
=== modified file 'lib/canonical/launchpad/subscribers/karma.py'
--- lib/canonical/launchpad/subscribers/karma.py 2010-01-22 01:53:02 +0000
+++ lib/canonical/launchpad/subscribers/karma.py 2010-08-17 20:02:42 +0000
@@ -6,9 +6,9 @@
from canonical.database.sqlbase import block_implicit_flushes
from canonical.launchpad.interfaces import BugTaskStatus
+from lp.bugs.subscribers.bug import get_bug_delta
from lp.code.enums import BranchMergeProposalStatus
from lp.registry.interfaces.person import IPerson
-from canonical.launchpad.mailnotification import get_bug_delta
@block_implicit_flushes
@@ -18,6 +18,7 @@
assert len(bug.bugtasks) >= 1
_assignKarmaUsingBugContext(IPerson(event.user), bug, 'bugcreated')
+
def _assign_karma_using_bugtask_context(person, bugtask, actionname):
"""Extract the right context from the bugtask and assign karma."""
distribution = bugtask.distribution
@@ -157,6 +158,7 @@
"""Assign karma to the user who registered the branch."""
branch.target.assignKarma(branch.registrant, 'branchcreated')
+
@block_implicit_flushes
def bug_branch_created(bug_branch, event):
"""Assign karma to the user who linked the bug to the branch."""
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-08-02 17:48:13 +0000
+++ lib/lp/bugs/configure.zcml 2010-08-17 20:02:42 +0000
@@ -51,25 +51,22 @@
handler="lp.bugs.subscribers.bugcreation.at_least_one_task"/>
<subscriber
for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectCreatedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_added"/>
+ handler="lp.bugs.subscribers.bug.notify_bug_added"/>
<subscriber
for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectCreatedEvent"
handler="canonical.launchpad.subscribers.karma.bug_created"/>
<subscriber
for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_modified"/>
- <subscriber
- for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
handler="canonical.launchpad.subscribers.karma.bug_modified"/>
<subscriber
for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
<subscriber
for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectCreatedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_attachment_added"/>
+ handler="lp.bugs.subscribers.bug.notify_bug_attachment_added"/>
<subscriber
for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectDeletedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_attachment_removed"/>
+ handler="lp.bugs.subscribers.bug.notify_bug_attachment_removed"/>
<subscriber
for="canonical.launchpad.interfaces.IBugAttachment lazr.lifecycle.interfaces.IObjectCreatedEvent"
handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
@@ -90,7 +87,7 @@
handler="canonical.launchpad.subscribers.karma.cve_added"/>
<subscriber
for="canonical.launchpad.interfaces.IBugMessage lazr.lifecycle.interfaces.IObjectCreatedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_comment_added"/>
+ handler="lp.bugs.subscribers.bug.notify_bug_comment_added"/>
<subscriber
for="canonical.launchpad.interfaces.IBugMessage lazr.lifecycle.interfaces.IObjectCreatedEvent"
handler="canonical.launchpad.subscribers.karma.bug_comment_added"/>
@@ -111,7 +108,7 @@
handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
<subscriber
for="canonical.launchpad.interfaces.IBugSubscription lazr.lifecycle.interfaces.IObjectCreatedEvent"
- handler="canonical.launchpad.mailnotification.notify_bug_subscription_added"/>
+ handler="lp.bugs.subscribers.bug.notify_bug_subscription_added"/>
<subscriber
for="canonical.launchpad.interfaces.IBug lazr.lifecycle.interfaces.IObjectModifiedEvent"
handler="lp.bugs.subscribers.bug.notify_bug_modified"/>
@@ -928,7 +925,7 @@
handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
<subscriber
for="canonical.launchpad.interfaces.IBugTask lazr.lifecycle.interfaces.IObjectModifiedEvent"
- handler="canonical.launchpad.mailnotification.notify_bugtask_edited"/>
+ handler="lp.bugs.subscribers.bugtask.notify_bugtask_edited"/>
<subscriber
for="canonical.launchpad.interfaces.IBugTask lazr.lifecycle.interfaces.IObjectModifiedEvent"
handler="canonical.launchpad.subscribers.karma.bugtask_modified"/>
=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt 2010-08-04 09:42:07 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt 2010-08-17 20:02:42 +0000
@@ -19,10 +19,8 @@
object it gets passed, the formatting logic has been cut into two
pieces: get_bug_changes and generate_bug_add_email.
- >>> from lp.bugs.adapters.bugchange import (
- ... get_bug_changes)
- >>> from canonical.launchpad.mailnotification import (
- ... generate_bug_add_email)
+ >>> from lp.bugs.adapters.bugchange import get_bug_changes
+ >>> from lp.bugs.mail.newbug import generate_bug_add_email
Let's demonstrate what the bugmails will look like, by going through
the various events that can happen that would cause a notification to
@@ -479,8 +477,7 @@
mailnotification.py contains a class, BugNotificationBuilder, which is
used to construct bug notification emails.
- >>> from canonical.launchpad.mailnotification import (
- ... BugNotificationBuilder)
+ >>> from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
When instantiatiated it derives a list of common unchanging headers
from the bug so that they are not calculated for every recipient.
=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt 2010-08-02 17:48:13 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt 2010-08-17 20:02:42 +0000
@@ -101,10 +101,8 @@
It is also possible to get the list of indirect subscribers for an
individual bug task.
- >>> from canonical.launchpad.mailnotification import (
- ... get_bugtask_indirect_subscribers)
- >>> get_bugtask_indirect_subscribers(
- ... linux_source_bug.bugtasks[0])
+ >>> from lp.bugs.subscribers.bug import get_bugtask_indirect_subscribers
+ >>> get_bugtask_indirect_subscribers(linux_source_bug.bugtasks[0])
[<Person at ...>]
The list of all bug subscribers can also be accessed via
=== added file 'lib/lp/bugs/mail/newbug.py'
--- lib/lp/bugs/mail/newbug.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/mail/newbug.py 2010-08-17 20:02:42 +0000
@@ -0,0 +1,96 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Mail for new bugs."""
+
+__metaclass__ = type
+__all__ = [
+ 'generate_bug_add_email',
+ ]
+
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.services.mail.mailwrapper import MailWrapper
+
+
+def generate_bug_add_email(bug, new_recipients=False, reason=None,
+ subscribed_by=None, event_creator=None):
+ """Generate a new bug notification from the given IBug.
+
+ If new_recipients is supplied we generate a notification explaining
+ that the new recipients have been subscribed to the bug. Otherwise
+ it's just a notification of a new bug report.
+ """
+ subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title)
+ contents = ''
+
+ if bug.private:
+ # This is a confidential bug.
+ visibility = u"Private"
+ else:
+ # This is a public bug.
+ visibility = u"Public"
+
+ if bug.security_related:
+ visibility += ' security'
+ contents += '*** This bug is a security vulnerability ***\n\n'
+
+ bug_info = []
+ # Add information about the affected upstreams and packages.
+ for bugtask in bug.bugtasks:
+ bug_info.append(u"** Affects: %s" % bugtask.bugtargetname)
+ bug_info.append(u" Importance: %s" % bugtask.importance.title)
+
+ if bugtask.assignee:
+ # There's a person assigned to fix this task, so show that
+ # information too.
+ bug_info.append(
+ u" Assignee: %s" % bugtask.assignee.unique_displayname)
+ bug_info.append(u" Status: %s\n" % bugtask.status.title)
+
+ if bug.tags:
+ bug_info.append('\n** Tags: %s' % ' '.join(bug.tags))
+
+ mailwrapper = MailWrapper(width=72)
+ content_substitutions = {
+ 'visibility': visibility,
+ 'bug_url': canonical_url(bug),
+ 'bug_info': "\n".join(bug_info),
+ 'bug_title': bug.title,
+ 'description': mailwrapper.format(bug.description),
+ 'notification_rationale': reason,
+ }
+
+ if new_recipients:
+ if "assignee" in reason:
+ contents += (
+ "You have been assigned a bug task for a %(visibility)s bug")
+ if event_creator is not None:
+ contents += " by %(assigner)s"
+ content_substitutions['assigner'] = (
+ event_creator.unique_displayname)
+ else:
+ contents += "You have been subscribed to a %(visibility)s bug"
+ if subscribed_by is not None:
+ contents += " by %(subscribed_by)s"
+ content_substitutions['subscribed_by'] = (
+ subscribed_by.unique_displayname)
+ contents += (":\n\n"
+ "%(description)s\n\n%(bug_info)s")
+ # The visibility appears mid-phrase so.. hack hack.
+ content_substitutions['visibility'] = visibility.lower()
+ # XXX: kiko, 2007-03-21:
+ # We should really have a centralized way of adding this
+ # footer, but right now we lack a INotificationRecipientSet
+ # for this particular situation.
+ contents += (
+ "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s")
+ else:
+ contents += ("%(visibility)s bug reported:\n\n"
+ "%(description)s\n\n%(bug_info)s")
+
+ contents = contents % content_substitutions
+
+ contents = contents.rstrip()
+
+ return (subject, contents)
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2010-08-17 20:02:41 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2010-08-17 20:02:42 +0000
@@ -22,15 +22,15 @@
from canonical.config import config
from canonical.launchpad.helpers import emailPeople, get_email_template
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
-from canonical.launchpad.mailnotification import (
- MailWrapper, generate_bug_add_email)
from canonical.launchpad.scripts.logger import log
from canonical.launchpad.webapp import canonical_url
from lp.bugs.interfaces.bugmessage import IBugMessageSet
from lp.bugs.mail.bugnotificationbuilder import (
BugNotificationBuilder, get_bugmail_from_address)
+from lp.bugs.mail.newbug import generate_bug_add_email
from lp.registry.interfaces.person import IPersonSet
+from lp.services.mail.mailwrapper import MailWrapper
def construct_email_notifications(bug_notifications):
=== modified file 'lib/lp/bugs/subscribers/bug.py'
--- lib/lp/bugs/subscribers/bug.py 2010-01-08 03:12:30 +0000
+++ lib/lp/bugs/subscribers/bug.py 2010-08-17 20:02:42 +0000
@@ -2,19 +2,57 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
-__all__ = ['notify_bug_modified']
-
-
+__all__ = [
+ 'add_bug_change_notifications',
+ 'get_bug_delta',
+ 'get_bugtask_indirect_subscribers',
+ 'notify_bug_added',
+ 'notify_bug_attachment_added',
+ 'notify_bug_attachment_removed',
+ 'notify_bug_comment_added',
+ 'notify_bug_modified',
+ 'notify_bug_subscription_added',
+ 'send_bug_details_to_new_bug_subscribers',
+ ]
+
+
+import datetime
+
+from operator import attrgetter
+
+from canonical.config import config
from canonical.database.sqlbase import block_implicit_flushes
+from canonical.launchpad.helpers import get_contact_email_addresses
+from canonical.launchpad.mail import format_address, sendmail
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.bugs.adapters.bugdelta import BugDelta
+from lp.bugs.mail.bugnotificationbuilder import BugNotificationBuilder
+from lp.bugs.mail.newbug import generate_bug_add_email
from lp.registry.interfaces.person import IPerson
+from lp.bugs.adapters.bugchange import (
+ BugDuplicateChange, BugTaskAssigneeChange, get_bug_changes)
+from lp.bugs.interfaces.bugchange import IBugChange
+from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
+from lp.registry.interfaces.structuralsubscription import (
+ BugNotificationLevel, IStructuralSubscriptionTarget)
+
+
+@block_implicit_flushes
+def notify_bug_added(bug, event):
+ """Send an email notification that a bug was added.
+
+ Event must be an IObjectCreatedEvent.
+ """
+ bug.addCommentNotification(bug.initial_message)
@block_implicit_flushes
def notify_bug_modified(bug, event):
"""Handle bug change events.
- Subscribe the security contacts for a bug when it
- becomes security-related.
+ Subscribe the security contacts for a bug when it becomes
+ security-related, and add notifications for the changes.
"""
if (event.object.security_related and
not event.object_before_modification.security_related):
@@ -23,3 +61,222 @@
for pillar in bug.affected_pillars:
if pillar.security_contact is not None:
bug.subscribe(pillar.security_contact, IPerson(event.user))
+
+ bug_delta = get_bug_delta(
+ old_bug=event.object_before_modification,
+ new_bug=event.object, user=IPerson(event.user))
+
+ if bug_delta is not None:
+ add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_comment_added(bugmessage, event):
+ """Notify CC'd list that a message was added to this bug.
+
+ bugmessage must be an IBugMessage. event must be an
+ IObjectCreatedEvent. If bugmessage.bug is a duplicate the
+ comment will also be sent to the dup target's subscribers.
+ """
+ bug = bugmessage.bug
+ bug.addCommentNotification(bugmessage.message)
+
+
+@block_implicit_flushes
+def notify_bug_attachment_added(bugattachment, event):
+ """Notify CC'd list that a new attachment has been added.
+
+ bugattachment must be an IBugAttachment. event must be an
+ IObjectCreatedEvent.
+ """
+ bug = bugattachment.bug
+ bug_delta = BugDelta(
+ bug=bug,
+ bugurl=canonical_url(bug),
+ user=IPerson(event.user),
+ attachment={'new': bugattachment, 'old': None})
+
+ add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_attachment_removed(bugattachment, event):
+ """Notify that an attachment has been removed."""
+ bug = bugattachment.bug
+ bug_delta = BugDelta(
+ bug=bug,
+ bugurl=canonical_url(bug),
+ user=IPerson(event.user),
+ attachment={'old': bugattachment, 'new': None})
+
+ add_bug_change_notifications(bug_delta)
+
+
+@block_implicit_flushes
+def notify_bug_subscription_added(bug_subscription, event):
+ """Notify that a new bug subscription was added."""
+ # When a user is subscribed to a bug by someone other
+ # than themselves, we send them a notification email.
+ if bug_subscription.person != bug_subscription.subscribed_by:
+ send_bug_details_to_new_bug_subscribers(
+ bug_subscription.bug, [], [bug_subscription.person],
+ subscribed_by=bug_subscription.subscribed_by)
+
+
+def get_bug_delta(old_bug, new_bug, user):
+ """Compute the delta from old_bug to new_bug.
+
+ old_bug and new_bug are IBug's. user is an IPerson. Returns an
+ IBugDelta if there are changes, or None if there were no changes.
+ """
+ changes = {}
+
+ for field_name in ("title", "description", "name", "private",
+ "security_related", "duplicateof", "tags"):
+ # fields for which we show old => new when their values change
+ old_val = getattr(old_bug, field_name)
+ new_val = getattr(new_bug, field_name)
+ if old_val != new_val:
+ changes[field_name] = {}
+ changes[field_name]["old"] = old_val
+ changes[field_name]["new"] = new_val
+
+ if changes:
+ changes["bug"] = new_bug
+ changes["bug_before_modification"] = old_bug
+ changes["bugurl"] = canonical_url(new_bug)
+ changes["user"] = user
+ return BugDelta(**changes)
+ else:
+ return None
+
+
+def get_bugtask_indirect_subscribers(bugtask, recipients=None, level=None):
+ """Return the indirect subscribers for a bug task.
+
+ Return the list of people who should get notifications about
+ changes to the task because of having an indirect subscription
+ relationship with it (by subscribing to its target, being an
+ assignee or owner, etc...)
+
+ If `recipients` is present, add the subscribers to the set of
+ bug notification recipients.
+ """
+ if bugtask.bug.private:
+ return set()
+
+ also_notified_subscribers = set()
+
+ # Assignees are indirect subscribers.
+ if bugtask.assignee:
+ also_notified_subscribers.add(bugtask.assignee)
+ if recipients is not None:
+ recipients.addAssignee(bugtask.assignee)
+
+ if IStructuralSubscriptionTarget.providedBy(bugtask.target):
+ also_notified_subscribers.update(
+ bugtask.target.getBugNotificationsRecipients(
+ recipients, level=level))
+
+ if bugtask.milestone is not None:
+ also_notified_subscribers.update(
+ bugtask.milestone.getBugNotificationsRecipients(
+ recipients, level=level))
+
+ # If the target's bug supervisor isn't set,
+ # we add the owner as a subscriber.
+ pillar = bugtask.pillar
+ if pillar.bug_supervisor is None:
+ also_notified_subscribers.add(pillar.owner)
+ if recipients is not None:
+ recipients.addRegistrant(pillar.owner, pillar)
+
+ return sorted(
+ also_notified_subscribers,
+ key=attrgetter('displayname'))
+
+
+def add_bug_change_notifications(bug_delta, old_bugtask=None,
+ new_subscribers=None):
+ """Generate bug notifications and add them to the bug."""
+ changes = get_bug_changes(bug_delta)
+ recipients = bug_delta.bug.getBugNotificationRecipients(
+ old_bug=bug_delta.bug_before_modification,
+ level=BugNotificationLevel.METADATA)
+ if old_bugtask is not None:
+ old_bugtask_recipients = BugNotificationRecipients()
+ get_bugtask_indirect_subscribers(
+ old_bugtask, recipients=old_bugtask_recipients,
+ level=BugNotificationLevel.METADATA)
+ recipients.update(old_bugtask_recipients)
+ for change in changes:
+ # XXX 2009-03-17 gmb [bug=344125]
+ # This if..else should be removed once the new BugChange API
+ # is complete and ubiquitous.
+ if IBugChange.providedBy(change):
+ if isinstance(change, BugDuplicateChange):
+ no_dupe_master_recipients = (
+ bug_delta.bug.getBugNotificationRecipients(
+ old_bug=bug_delta.bug_before_modification,
+ level=BugNotificationLevel.METADATA,
+ include_master_dupe_subscribers=False))
+ bug_delta.bug.addChange(
+ change, recipients=no_dupe_master_recipients)
+ elif (isinstance(change, BugTaskAssigneeChange) and
+ new_subscribers is not None):
+ for person in new_subscribers:
+ reason, rationale = recipients.getReason(person)
+ if 'Assignee' in rationale:
+ recipients.remove(person)
+ bug_delta.bug.addChange(change, recipients=recipients)
+ else:
+ bug_delta.bug.addChange(change, recipients=recipients)
+ else:
+ bug_delta.bug.addChangeNotification(
+ change, person=bug_delta.user, recipients=recipients)
+
+
+def send_bug_details_to_new_bug_subscribers(
+ bug, previous_subscribers, current_subscribers, subscribed_by=None,
+ event_creator=None):
+ """Send an email containing full bug details to new bug subscribers.
+
+ This function is designed to handle situations where bugtasks get
+ reassigned to new products or sourcepackages, and the new bug subscribers
+ need to be notified of the bug.
+ """
+ prev_subs_set = set(previous_subscribers)
+ cur_subs_set = set(current_subscribers)
+ new_subs = cur_subs_set.difference(prev_subs_set)
+
+ to_addrs = set()
+ for new_sub in new_subs:
+ to_addrs.update(get_contact_email_addresses(new_sub))
+
+ if not to_addrs:
+ return
+
+ from_addr = format_address(
+ 'Launchpad Bug Tracker',
+ "%s@%s" % (bug.id, config.launchpad.bugs_domain))
+ # Now's a good a time as any for this email; don't use the original
+ # reported date for the bug as it will just confuse mailer and
+ # recipient.
+ email_date = datetime.datetime.now()
+
+ # The new subscriber email is effectively the initial message regarding
+ # a new bug. The bug's initial message is used in the References
+ # header to establish the message's context in the email client.
+ references = [bug.initial_message.rfc822msgid]
+ recipients = bug.getBugNotificationRecipients()
+
+ bug_notification_builder = BugNotificationBuilder(bug, event_creator)
+ for to_addr in sorted(to_addrs):
+ reason, rationale = recipients.getReason(to_addr)
+ subject, contents = generate_bug_add_email(
+ bug, new_recipients=True, subscribed_by=subscribed_by,
+ reason=reason, event_creator=event_creator)
+ msg = bug_notification_builder.build(
+ from_addr, to_addr, contents, subject, email_date,
+ rationale=rationale, references=references)
+ sendmail(msg)
=== modified file 'lib/lp/bugs/subscribers/bugcreation.py'
--- lib/lp/bugs/subscribers/bugcreation.py 2009-06-25 00:40:31 +0000
+++ lib/lp/bugs/subscribers/bugcreation.py 2010-08-17 20:02:42 +0000
@@ -2,9 +2,13 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
+__all__ = [
+ 'at_least_one_task',
+ ]
-from canonical.database.sqlbase import block_implicit_flushes
from lp.bugs.interfaces.bug import CreatedBugWithNoBugTasksError
+
+
def at_least_one_task(bug, event):
"""Make sure that the created bug has at least one task.
=== added file 'lib/lp/bugs/subscribers/bugtask.py'
--- lib/lp/bugs/subscribers/bugtask.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/subscribers/bugtask.py 2010-08-17 20:02:42 +0000
@@ -0,0 +1,78 @@
+# Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'notify_bugtask_edited',
+ 'update_security_contact_subscriptions',
+ ]
+
+
+from canonical.database.sqlbase import block_implicit_flushes
+from canonical.launchpad.webapp.publisher import canonical_url
+
+from lp.bugs.adapters.bugdelta import BugDelta
+from lp.bugs.interfaces.bugtask import IUpstreamBugTask
+from lp.bugs.subscribers.bug import (
+ add_bug_change_notifications, send_bug_details_to_new_bug_subscribers)
+from lp.registry.interfaces.person import IPerson
+
+
+@block_implicit_flushes
+def update_security_contact_subscriptions(event):
+ """Subscribe the new security contact when a bugtask's product changes.
+
+ Only subscribes the new security contact if the bug was marked a
+ security issue originally.
+
+ No change is made for private bugs.
+ """
+ if event.object.bug.private:
+ return
+
+ if not IUpstreamBugTask.providedBy(event.object):
+ return
+
+ bugtask_before_modification = event.object_before_modification
+ bugtask_after_modification = event.object
+
+ if (bugtask_before_modification.product !=
+ bugtask_after_modification.product):
+ new_product = bugtask_after_modification.product
+ if (bugtask_before_modification.bug.security_related and
+ new_product.security_contact):
+ bugtask_after_modification.bug.subscribe(
+ new_product.security_contact, IPerson(event.user))
+
+
+@block_implicit_flushes
+def notify_bugtask_edited(modified_bugtask, event):
+ """Notify CC'd subscribers of this bug that something has changed
+ on this task.
+
+ modified_bugtask must be an IBugTask. event must be an
+ IObjectModifiedEvent.
+ """
+ bugtask_delta = event.object.getDelta(event.object_before_modification)
+ bug_delta = BugDelta(
+ bug=event.object.bug,
+ bugurl=canonical_url(event.object.bug),
+ bugtask_deltas=bugtask_delta,
+ user=IPerson(event.user))
+
+ event_creator = IPerson(event.user)
+ previous_subscribers = event.object_before_modification.bug_subscribers
+ current_subscribers = event.object.bug_subscribers
+ prev_subs_set = set(previous_subscribers)
+ cur_subs_set = set(current_subscribers)
+ new_subs = cur_subs_set.difference(prev_subs_set)
+
+ add_bug_change_notifications(
+ bug_delta, old_bugtask=event.object_before_modification,
+ new_subscribers=new_subs)
+
+ send_bug_details_to_new_bug_subscribers(
+ event.object.bug, previous_subscribers, current_subscribers,
+ event_creator=event_creator)
+
+ update_security_contact_subscriptions(event)
Follow ups