Merge lp:~jtv/launchpad/pre-876594 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 14202
Proposed branch: lp:~jtv/launchpad/pre-876594
Merge into: lp:launchpad
Diff against target: 646 lines (+203/-174)
7 files modified
lib/lp/archiveuploader/tests/nascentupload-announcements.txt (+4/-4)
lib/lp/soyuz/adapters/notification.py (+82/-78)
lib/lp/soyuz/adapters/tests/test_notification.py (+70/-66)
lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt (+2/-0)
lib/lp/soyuz/doc/distroseriesqueue-notify.txt (+40/-19)
lib/lp/soyuz/model/queue.py (+1/-1)
lib/lp/soyuz/scripts/packagecopier.py (+4/-6)
To merge this branch: bzr merge lp:~jtv/launchpad/pre-876594
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+80537@code.launchpad.net

Commit message

[r=lifeless][no-qa] Cleanups for package upload notification.

Description of the change

= Summary =

We need to make some changes to Soyuz upload notification. Actually this also covers copying, building, and syncing of packages.

Unfortunately the code has become a bit of a mess and some of the tests are painfully brittle.

== Proposed fix ==

Before fixing the bug itself, clean up the code. Make tests independent of arbitrary ordering of email addressee lists; eliminate needless repetition of code; make the name of the function we'll be changing a bit more specific; make complicated code a bit more concise so we can reason about it more effectively.

== Pre-implementation notes ==

Discussed the bigger picture with Julian. We're going to exempt package maintainers and authors from notification for packages that are being accepted into different archives than their releases are in. We'll also want to see if we can remove complexity from notify() that reflects knowledge inherent in the callsite.

== Implementation details ==

The recipients-gathering code no longer checks for various recipients being the same. Instead it just lumps all recipients into a set. Duplicates disappear as a matter of course.

This will affect the logging a bit: the code will now happily say it's adding both the maintainer and the last author to the set of recipients, even if they're the same person. It may actually be clearer; certainly the code becomes easier to manage.

== Tests ==

{{{
./bin/test -vvc lp.archiveuploader
./bin/test -vvc lp.soyuz
}}}

== Demo and Q/A ==

There are no functional changes, so as long as nothing oopses, it should be fine.

= Launchpad lint =

I'll address the lint in distroseriesqueue-notify.txt in a separate lint branch. The other stuff is a bit harder to clean up because it involves whitespace-sensitive tests.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/scripts/packagecopier.py
  lib/lp/soyuz/model/queue.py
  lib/lp/archiveuploader/tests/nascentupload-announcements.txt
  lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt
  lib/lp/soyuz/adapters/notification.py
  lib/lp/soyuz/doc/distroseriesqueue-notify.txt
  lib/lp/soyuz/adapters/tests/test_notification.py

./lib/lp/archiveuploader/tests/nascentupload-announcements.txt
     186: want exceeds 78 characters.
     187: want exceeds 78 characters.
     659: want exceeds 78 characters.
     726: want exceeds 78 characters.
     728: want exceeds 78 characters.
     774: want exceeds 78 characters.
     776: want exceeds 78 characters.
./lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt
     145: want exceeds 78 characters.
     148: want exceeds 78 characters.
./lib/lp/soyuz/doc/distroseriesqueue-notify.txt
       1: narrative uses a moin header.
       6: source has bad indentation.
       9: narrative has trailing whitespace.
      10: narrative has trailing whitespace.
      13: source has bad indentation.
      21: source exceeds 78 characters.
      21: source has bad indentation.
      41: source has bad indentation.
      60: source has bad indentation.
      67: source has bad indentation.
      74: source has bad indentation.
      79: source has bad indentation.
      84: source has bad indentation.
     102: narrative has trailing whitespace.
     103: narrative has trailing whitespace.
     106: source has bad indentation.
     113: source has bad indentation.
     133: source has bad indentation.
     140: source has bad indentation.
     144: source has bad indentation.
     150: source has bad indentation.
     155: source has bad indentation.
     161: source has bad indentation.
     168: source has bad indentation.
     173: source has bad indentation.
     178: source has bad indentation.
     200: source has bad indentation.
     218: source has bad indentation.
     222: source has bad indentation.
     229: source has bad indentation.
     234: source has bad indentation.
     256: source has bad indentation.
     277: source has bad indentation.
     283: source has bad indentation.
./lib/lp/soyuz/adapters/tests/test_notification.py
     217: Line exceeds 78 characters.
     218: Line exceeds 78 characters.
     222: Line exceeds 78 characters.
     217: E501 line too long (85 characters)
     218: E501 line too long (82 characters)
     222: E501 line too long (82 characters)

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

10 + ... for entry in sorted([addr.strip() for addr in field.split(',')]):
11 + ... print entry

and also in
509 + >>> def extract_addresses(header_field):

might be better as
for entry in sorted(re.split(field, ' \t\r\n,')):

86 + # Some syncs (e.g. from Debian) will involve packages whose
87 + # changed-by person was auto-created in LP and hence does not have a
88 + # preferred email address set.
89 + changedby_person = email_to_person(info['changedby'])

The comment here now looks wonky - there is no special casing happening, so perhaps it should move to email_to_person?

I think this existing comment:
196 # Now filter list of recipients for persons only registered in
197 # Launchpad to avoid spamming the innocent.

means 'Do not email persons that have not configured a preferred email - e.g. folk listed in Launchpad who have not activated their account'. If so, and if my phrasing is clearer, feel free to update it.

For this:
231 + if fullemail is None or fullemail == '':
232 + return None
I would write 'if not fullemail:' - thats what __nonzero__ is for, and its correct here. Our style guide was updated a while ago to remove the admonition that this was bad (its not).

person_to_email looks like something other bits of code (e.g. incoming-mail-processing) will want/need; since you are here you could consider deduping.

Looks fine to me, these are all tweaks.

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

Thanks for the fast review.

> 10 + ... for entry in sorted([addr.strip() for addr in
> field.split(',')]):
> 11 + ... print entry
>
> and also in
> 509 + >>> def extract_addresses(header_field):
>
> might be better as
> for entry in sorted(re.split(field, ' \t\r\n,')):

Alas, no. The field may contain a full name, so it generally contains spaces that should not be split.

> 86 + # Some syncs (e.g. from Debian) will involve packages whose
> 87 + # changed-by person was auto-created in LP and hence does not have a
> 88 + # preferred email address set.
> 89 + changedby_person = email_to_person(info['changedby'])
>
> The comment here now looks wonky - there is no special casing happening, so
> perhaps it should move to email_to_person?

Doesn't really make sense to talk about the changed-by author there. Instead, I extended the comment to say that in that situation email_to_person will return None. Puts the rest of the comment in perspective.

> I think this existing comment:
> 196 # Now filter list of recipients for persons only registered in
> 197 # Launchpad to avoid spamming the innocent.
>
> means 'Do not email persons that have not configured a preferred email - e.g.
> folk listed in Launchpad who have not activated their account'. If so, and if
> my phrasing is clearer, feel free to update it.

Did that.

> For this:
> 231 + if fullemail is None or fullemail == '':
> 232 + return None
> I would write 'if not fullemail:' - thats what __nonzero__ is for, and its
> correct here. Our style guide was updated a while ago to remove the admonition
> that this was bad (its not).

Certainly easier. Changed.

> person_to_email looks like something other bits of code (e.g. incoming-mail-
> processing) will want/need; since you are here you could consider deduping.

Probably not worth the trouble for a simple two-liner.

Jeroen

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/archiveuploader/tests/nascentupload-announcements.txt'
--- lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2011-08-22 04:38:02 +0000
+++ lib/lp/archiveuploader/tests/nascentupload-announcements.txt 2011-10-27 16:44:31 +0000
@@ -46,8 +46,8 @@
46 ... return cmp(a[1], b[1])46 ... return cmp(a[1], b[1])
4747
48 >>> def print_addrlist(field):48 >>> def print_addrlist(field):
49 ... for entry in field.split(','):49 ... for entry in sorted([addr.strip() for addr in field.split(',')]):
50 ... print entry.strip()50 ... print entry
5151
52Import the test keys to use 'insecure' policy.52Import the test keys to use 'insecure' policy.
5353
@@ -443,8 +443,8 @@
443 'Launchpad actually'443 'Launchpad actually'
444444
445 >>> print_addrlist(notification['To'])445 >>> print_addrlist(notification['To'])
446 Daniel Silverstone <daniel.silverstone@canonical.com>
446 Foo Bar <foo.bar@canonical.com>447 Foo Bar <foo.bar@canonical.com>
447 Daniel Silverstone <daniel.silverstone@canonical.com>
448448
449 >>> notification['Subject']449 >>> notification['Subject']
450 '[ubuntu/hoary-updates] bar 1.0-2 (Waiting for approval)'450 '[ubuntu/hoary-updates] bar 1.0-2 (Waiting for approval)'
@@ -479,8 +479,8 @@
479 'Launchpad actually'479 'Launchpad actually'
480480
481 >>> print_addrlist(notification['To'])481 >>> print_addrlist(notification['To'])
482 Daniel Silverstone <daniel.silverstone@canonical.com>
482 Foo Bar <foo.bar@canonical.com>483 Foo Bar <foo.bar@canonical.com>
483 Daniel Silverstone <daniel.silverstone@canonical.com>
484484
485 >>> notification['Subject']485 >>> notification['Subject']
486 '[ubuntu/hoary-backports] bar 1.0-3 (Waiting for approval)'486 '[ubuntu/hoary-backports] bar 1.0-3 (Waiting for approval)'
487487
=== modified file 'lib/lp/soyuz/adapters/notification.py'
--- lib/lp/soyuz/adapters/notification.py 2011-08-31 05:06:54 +0000
+++ lib/lp/soyuz/adapters/notification.py 2011-10-27 16:44:31 +0000
@@ -6,7 +6,6 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'get_recipients', # Available for testing only.
10 'notify',9 'notify',
11 ]10 ]
1211
@@ -68,7 +67,7 @@
68 }67 }
69 template = get_template(archive, 'rejected')68 template = get_template(archive, 'rejected')
70 body = template % information69 body = template % information
71 to_addrs = get_recipients(70 to_addrs = get_upload_notification_recipients(
72 blamer, archive, distroseries, logger, changes=changes)71 blamer, archive, distroseries, logger, changes=changes)
73 debug(logger, "Sending rejection email.")72 debug(logger, "Sending rejection email.")
74 if not to_addrs:73 if not to_addrs:
@@ -128,7 +127,7 @@
128 summary_text=None, changes=None, changesfile_content=None,127 summary_text=None, changes=None, changesfile_content=None,
129 changesfile_object=None, action=None, dry_run=False,128 changesfile_object=None, action=None, dry_run=False,
130 logger=None, announce_from_person=None, previous_version=None):129 logger=None, announce_from_person=None, previous_version=None):
131 """Notify about130 """Notify about an upload or package copy.
132131
133 :param blamer: The `IPerson` who is to blame for this notification.132 :param blamer: The `IPerson` who is to blame for this notification.
134 :param spr: The `ISourcePackageRelease` that was created.133 :param spr: The `ISourcePackageRelease` that was created.
@@ -187,7 +186,7 @@
187 if not files and action != 'rejected':186 if not files and action != 'rejected':
188 return187 return
189188
190 recipients = get_recipients(189 recipients = get_upload_notification_recipients(
191 blamer, archive, distroseries, logger, changes=changes, spr=spr,190 blamer, archive, distroseries, logger, changes=changes, spr=spr,
192 bprs=bprs)191 bprs=bprs)
193192
@@ -304,13 +303,12 @@
304 if distroseries.changeslist:303 if distroseries.changeslist:
305 information['ANNOUNCE'] = "Announcing to %s" % (304 information['ANNOUNCE'] = "Announcing to %s" % (
306 distroseries.changeslist)305 distroseries.changeslist)
307 try:306
308 changedby_person = email_to_person(info['changedby'])307 # Some syncs (e.g. from Debian) will involve packages whose
309 except ParseMaintError:308 # changed-by person was auto-created in LP and hence does not have a
310 # Some syncs (e.g. from Debian) will involve packages whose309 # preferred email address set. We'll get a None here.
311 # changed-by person was auto-created in LP and hence does not310 changedby_person = email_to_person(info['changedby'])
312 # have a preferred email address set.311
313 changedby_person = None
314 if blamer is not None and blamer != changedby_person:312 if blamer is not None and blamer != changedby_person:
315 signer_signature = person_to_email(blamer)313 signer_signature = person_to_email(blamer)
316 if signer_signature != info['changedby']:314 if signer_signature != info['changedby']:
@@ -448,73 +446,69 @@
448 return guess_encoding(s)446 return guess_encoding(s)
449447
450448
451def debug(logger, msg):449def debug(logger, msg, *args, **kwargs):
452 """Shorthand debug notation for publish() methods."""450 """Shorthand debug notation for publish() methods."""
453 if logger is not None:451 if logger is not None:
454 logger.debug(msg)452 logger.debug(msg, *args, **kwargs)
455453
456454
457def get_recipients(blamer, archive, distroseries, logger, changes=None,455def is_valid_uploader(person, distribution):
458 spr=None, bprs=None):456 """Is `person` an uploader for `distribution`?
457
458 A `None` person is not an uploader.
459 """
460 if person is None:
461 return None
462 else:
463 return person.isUploader(distribution)
464
465
466def get_upload_notification_recipients(blamer, archive, distroseries,
467 logger=None, changes=None, spr=None,
468 bprs=None):
459 """Return a list of recipients for notification emails."""469 """Return a list of recipients for notification emails."""
460 candidate_recipients = []
461 debug(logger, "Building recipients list.")470 debug(logger, "Building recipients list.")
471 candidate_recipients = [
472 blamer,
473 ]
462 info = fetch_information(spr, bprs, changes)474 info = fetch_information(spr, bprs, changes)
463475
464 if info['changedby']:476 changer = email_to_person(info['changedby'])
465 try:477 maintainer = email_to_person(info['maintainer'])
466 changer = email_to_person(info['changedby'])478
467 except ParseMaintError:479 if blamer is None:
468 changer = None480 debug(
469 else:481 logger, "Changes file is unsigned; adding changer as recipient.")
470 changer = None
471
472 if info['maintainer']:
473 try:
474 maintainer = email_to_person(info['maintainer'])
475 except ParseMaintError:
476 maintainer = None
477 else:
478 maintainer = None
479
480 if blamer:
481 # This is a signed upload.
482 candidate_recipients.append(blamer)
483 else:
484 debug(logger,
485 "Changes file is unsigned, adding changer as recipient")
486 candidate_recipients.append(changer)482 candidate_recipients.append(changer)
487483
488 if archive.is_ppa:484 if archive.is_ppa:
489 # For PPAs, any person or team mentioned explicitly in the485 # For PPAs, any person or team mentioned explicitly in the
490 # ArchivePermissions as uploaders for the archive will also486 # ArchivePermissions as uploaders for the archive will also
491 # get emailed.487 # get emailed.
492 uploaders = [488 candidate_recipients.extend([
493 permission.person for permission in489 permission.person
494 archive.getUploadersForComponent()]490 for permission in archive.getUploadersForComponent()])
495 candidate_recipients.extend(uploaders)491 else:
496492 # If this is not a PPA, we also consider maintainer and changed-by.
497 # If this is not a PPA, we also consider maintainer and changed-by.493 if blamer is not None:
498 elif blamer is not None:494 if is_valid_uploader(maintainer, distroseries.distribution):
499 if (maintainer and maintainer != blamer and495 debug(logger, "Adding maintainer to recipients")
500 maintainer.isUploader(distroseries.distribution)):496 candidate_recipients.append(maintainer)
501 debug(logger, "Adding maintainer to recipients")497
502 candidate_recipients.append(maintainer)498 if is_valid_uploader(changer, distroseries.distribution):
503499 debug(logger, "Adding changed-by to recipients")
504 if (changer and changer != blamer and500 candidate_recipients.append(changer)
505 changer.isUploader(distroseries.distribution)):501
506 debug(logger, "Adding changed-by to recipients")502 # Collect email addresses to notify. Skip persons who do not have a
507 candidate_recipients.append(changer)503 # preferredemail set, such as people who have not activated their
508504 # Launchpad accounts (and are therefore not expecting this email).
509 # Now filter list of recipients for persons only registered in505 recipients = [
510 # Launchpad to avoid spamming the innocent.506 format_address_for_person(person)
511 recipients = []507 for person in filter(None, set(candidate_recipients))
512 for person in candidate_recipients:508 if person.preferredemail is not None]
513 if person is None or person.preferredemail is None:509
514 continue510 for recipient in recipients:
515 recipient = format_address_for_person(person)511 debug(logger, "Adding recipient: '%s'", recipient)
516 debug(logger, "Adding recipient: '%s'" % recipient)
517 recipients.append(recipient)
518512
519 return recipients513 return recipients
520514
@@ -572,18 +566,29 @@
572566
573567
574def email_to_person(fullemail):568def email_to_person(fullemail):
575 """Return an IPerson given an RFC2047 email address."""569 """Return an `IPerson` given an RFC2047 email address.
576 # The 2nd arg to s_f_m() doesn't matter as it won't fail since every-570
577 # thing will have already parsed at this point.571 :param fullemail: Potential email address.
578 (rfc822, rfc2047, name, email) = safe_fix_maintainer(572 :return: `IPerson` with the given email address. None if there
579 fullemail, "email")573 isn't one, or if `fullemail` isn't a proper email address.
580 return getUtility(IPersonSet).getByEmail(email)574 """
575 if not fullemail:
576 return None
577
578 try:
579 # The 2nd arg to s_f_m() doesn't matter as it won't fail since every-
580 # thing will have already parsed at this point.
581 (rfc822, rfc2047, name, email) = safe_fix_maintainer(
582 fullemail, "email")
583 return getUtility(IPersonSet).getByEmail(email)
584 except ParseMaintError:
585 return None
581586
582587
583def person_to_email(person):588def person_to_email(person):
584 """Return a string of full name <e-mail address> given an IPerson."""589 """Return a string of full name <e-mail address> given an IPerson."""
585 # This will use email.Header to encode any unicode.
586 if person and person.preferredemail:590 if person and person.preferredemail:
591 # This will use email.Header to encode any non-ASCII characters.
587 return format_address_for_person(person)592 return format_address_for_person(person)
588593
589594
@@ -595,12 +600,11 @@
595 user (archive@ubuntu.com).600 user (archive@ubuntu.com).
596 """601 """
597 katie = getUtility(ILaunchpadCelebrities).katie602 katie = getUtility(ILaunchpadCelebrities).katie
598 try:603 changed_by = email_to_person(changed_by_email)
599 changed_by = email_to_person(changed_by_email)
600 except ParseMaintError:
601 return False
602 return (604 return (
603 spr and not bprs and changed_by == katie and605 spr and
606 not bprs and
607 changed_by == katie and
604 pocket != PackagePublishingPocket.SECURITY)608 pocket != PackagePublishingPocket.SECURITY)
605609
606610
607611
=== modified file 'lib/lp/soyuz/adapters/tests/test_notification.py'
--- lib/lp/soyuz/adapters/tests/test_notification.py 2011-08-31 05:06:54 +0000
+++ lib/lp/soyuz/adapters/tests/test_notification.py 2011-10-27 16:44:31 +0000
@@ -6,8 +6,9 @@
6# GNU Affero General Public License version 3 (see the file LICENSE).6# GNU Affero General Public License version 3 (see the file LICENSE).
77
8from email.utils import formataddr8from email.utils import formataddr
9from textwrap import dedent
10
9from storm.store import Store11from storm.store import Store
10from textwrap import dedent
11from zope.component import getUtility12from zope.component import getUtility
12from zope.security.proxy import removeSecurityProxy13from zope.security.proxy import removeSecurityProxy
1314
@@ -18,24 +19,24 @@
18 )19 )
19from lp.archivepublisher.utils import get_ppa_reference20from lp.archivepublisher.utils import get_ppa_reference
20from lp.registry.interfaces.pocket import PackagePublishingPocket21from lp.registry.interfaces.pocket import PackagePublishingPocket
22from lp.services.log.logger import BufferLogger
21from lp.services.mail.sendmail import format_address_for_person23from lp.services.mail.sendmail import format_address_for_person
22from lp.services.log.logger import BufferLogger
23from lp.soyuz.adapters.notification import (24from lp.soyuz.adapters.notification import (
24 assemble_body,25 assemble_body,
25 calculate_subject,26 calculate_subject,
26 get_recipients,
27 fetch_information,27 fetch_information,
28 get_upload_notification_recipients,
28 is_auto_sync_upload,29 is_auto_sync_upload,
30 notify,
31 person_to_email,
29 reject_changes_file,32 reject_changes_file,
30 person_to_email,
31 notify,
32 )33 )
33from lp.soyuz.interfaces.component import IComponentSet
34from lp.soyuz.model.component import ComponentSelection
35from lp.soyuz.enums import (34from lp.soyuz.enums import (
36 ArchivePurpose,35 ArchivePurpose,
37 PackageUploadCustomFormat,36 PackageUploadCustomFormat,
38 )37 )
38from lp.soyuz.interfaces.component import IComponentSet
39from lp.soyuz.model.component import ComponentSelection
39from lp.soyuz.model.distroseriessourcepackagerelease import (40from lp.soyuz.model.distroseriessourcepackagerelease import (
40 DistroSeriesSourcePackageRelease,41 DistroSeriesSourcePackageRelease,
41 )42 )
@@ -231,8 +232,8 @@
231 def test_fetch_information_changes(self):232 def test_fetch_information_changes(self):
232 changes = {233 changes = {
233 'Date': '2001-01-01',234 'Date': '2001-01-01',
234 'Changed-By': 'Foo Bar <foo.bar@canonical.com>',235 'Changed-By': 'Foo Bar <foo.bar@example.com>',
235 'Maintainer': 'Foo Bar <foo.bar@canonical.com>',236 'Maintainer': 'Foo Bar <foo.bar@example.com>',
236 'Changes': ' * Foo!',237 'Changes': ' * Foo!',
237 }238 }
238 info = fetch_information(239 info = fetch_information(
@@ -246,7 +247,7 @@
246 info['maintainer_displayname'],247 info['maintainer_displayname'],
247 ]248 ]
248 for field in fields:249 for field in fields:
249 self.assertEqual('Foo Bar <foo.bar@canonical.com>', field)250 self.assertEqual('Foo Bar <foo.bar@example.com>', field)
250251
251 def test_fetch_information_spr(self):252 def test_fetch_information_spr(self):
252 creator = self.factory.makePerson(displayname=u"foø")253 creator = self.factory.makePerson(displayname=u"foø")
@@ -365,64 +366,67 @@
365 distroseries=distroseries, component=component))366 distroseries=distroseries, component=component))
366 archive.newComponentUploader(maintainer, component)367 archive.newComponentUploader(maintainer, component)
367 archive.newComponentUploader(changer, component)368 archive.newComponentUploader(changer, component)
368 return get_recipients(369 return get_upload_notification_recipients(
369 blamer, archive, distroseries, logger=None, changes=changes)370 blamer, archive, distroseries, logger=None, changes=changes)
370371
371 def test_get_recipients_good_emails(self):372 def test_get_upload_notification_recipients_good_emails(self):
372 # Test get_recipients with good email addresses..373 # Test get_upload_notification_recipients with good email addresses..
373 blamer = self.factory.makePerson()374 blamer = self.factory.makePerson()
374 maintainer = self.factory.makePerson(375 maintainer = self.factory.makePerson(
375 'maintainer@canonical.com', displayname='Maintainer')376 'maintainer@example.com', displayname='Maintainer')
376 changer = self.factory.makePerson(377 changer = self.factory.makePerson(
377 'changer@canonical.com', displayname='Changer')378 'changer@example.com', displayname='Changer')
378 changes = {379 changes = {
379 'Date': '2001-01-01',380 'Date': '2001-01-01',
380 'Changed-By': 'Changer <changer@canonical.com>',381 'Changed-By': 'Changer <changer@example.com>',
381 'Maintainer': 'Maintainer <maintainer@canonical.com>',382 'Maintainer': 'Maintainer <maintainer@example.com>',
382 'Changes': ' * Foo!',383 'Changes': ' * Foo!',
383 }384 }
384 recipients = self._run_recipients_test(385 recipients = self._run_recipients_test(
385 changes, blamer, maintainer, changer)386 changes, blamer, maintainer, changer)
386 expected = [format_address_for_person(p)387 expected = [
387 for p in (blamer, maintainer, changer)]388 format_address_for_person(person)
388 self.assertEqual(expected, recipients)389 for person in (blamer, maintainer, changer)]
389390 self.assertContentEqual(expected, recipients)
390 def test_get_recipients_bad_maintainer_email(self):391
391 blamer = self.factory.makePerson()392 def test_get_upload_notification_recipients_bad_maintainer_email(self):
392 maintainer = self.factory.makePerson(393 blamer = self.factory.makePerson()
393 'maintainer@canonical.com', displayname='Maintainer')394 maintainer = self.factory.makePerson(
394 changer = self.factory.makePerson(395 'maintainer@example.com', displayname='Maintainer')
395 'changer@canonical.com', displayname='Changer')396 changer = self.factory.makePerson(
396 changes = {397 'changer@example.com', displayname='Changer')
397 'Date': '2001-01-01',398 changes = {
398 'Changed-By': 'Changer <changer@canonical.com>',399 'Date': '2001-01-01',
399 'Maintainer': 'Maintainer <maintainer at canonical.com>',400 'Changed-By': 'Changer <changer@example.com>',
400 'Changes': ' * Foo!',401 'Maintainer': 'Maintainer <maintainer at example.com>',
401 }402 'Changes': ' * Foo!',
402 recipients = self._run_recipients_test(403 }
403 changes, blamer, maintainer, changer)404 recipients = self._run_recipients_test(
404 expected = [format_address_for_person(p)405 changes, blamer, maintainer, changer)
405 for p in (blamer, changer)]406 expected = [
406 self.assertEqual(expected, recipients)407 format_address_for_person(person) for person in (blamer, changer)]
407408 self.assertContentEqual(expected, recipients)
408 def test_get_recipients_bad_changedby_email(self):409
409 # Test get_recipients with invalid changedby email address.410 def test_get_upload_notification_recipients_bad_changedby_email(self):
410 blamer = self.factory.makePerson()411 # Test get_upload_notification_recipients with invalid changedby
411 maintainer = self.factory.makePerson(412 # email address.
412 'maintainer@canonical.com', displayname='Maintainer')413 blamer = self.factory.makePerson()
413 changer = self.factory.makePerson(414 maintainer = self.factory.makePerson(
414 'changer@canonical.com', displayname='Changer')415 'maintainer@example.com', displayname='Maintainer')
415 changes = {416 changer = self.factory.makePerson(
416 'Date': '2001-01-01',417 'changer@example.com', displayname='Changer')
417 'Changed-By': 'Changer <changer at canonical.com>',418 changes = {
418 'Maintainer': 'Maintainer <maintainer@canonical.com>',419 'Date': '2001-01-01',
419 'Changes': ' * Foo!',420 'Changed-By': 'Changer <changer at example.com>',
420 }421 'Maintainer': 'Maintainer <maintainer@example.com>',
421 recipients = self._run_recipients_test(422 'Changes': ' * Foo!',
422 changes, blamer, maintainer, changer)423 }
423 expected = [format_address_for_person(p)424 recipients = self._run_recipients_test(
424 for p in (blamer, maintainer)]425 changes, blamer, maintainer, changer)
425 self.assertEqual(expected, recipients)426 expected = [
427 format_address_for_person(person)
428 for person in (blamer, maintainer)]
429 self.assertContentEqual(expected, recipients)
426430
427 def test_assemble_body_handles_no_preferred_email_for_changer(self):431 def test_assemble_body_handles_no_preferred_email_for_changer(self):
428 # If changer has no preferred email address,432 # If changer has no preferred email address,
429433
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2011-07-21 23:10:26 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-dist-upgrader.txt 2011-10-27 16:44:31 +0000
@@ -60,6 +60,7 @@
60 DEBUG Creating queue entry60 DEBUG Creating queue entry
61 DEBUG Setting it to ACCEPTED61 DEBUG Setting it to ACCEPTED
62 DEBUG Building recipients list.62 DEBUG Building recipients list.
63 ...
63 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'64 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'
64 DEBUG Sent a mail:65 DEBUG Sent a mail:
65 ...66 ...
@@ -235,6 +236,7 @@
235 DEBUG Creating queue entry236 DEBUG Creating queue entry
236 DEBUG Setting it to ACCEPTED237 DEBUG Setting it to ACCEPTED
237 DEBUG Building recipients list.238 DEBUG Building recipients list.
239 ...
238 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'240 DEBUG Adding recipient: 'Foo Bar <foo.bar@canonical.com>'
239 DEBUG Sent a mail:241 DEBUG Sent a mail:
240 ...242 ...
241243
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-notify.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2011-06-01 04:57:27 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-notify.txt 2011-10-27 16:44:31 +0000
@@ -45,11 +45,11 @@
45 >>> netapplet_upload.notify(45 >>> netapplet_upload.notify(
46 ... changes_file_object=changes_file, logger=FakeLogger())46 ... changes_file_object=changes_file, logger=FakeLogger())
47 DEBUG Building recipients list.47 DEBUG Building recipients list.
48 DEBUG Changes file is unsigned, adding changer as recipient48 DEBUG Changes file is unsigned; adding changer as recipient.
49 ...49 ...
50 DEBUG Sent a mail:50 DEBUG Sent a mail:
51 ...51 ...
52 DEBUG Recipients: Daniel Silverstone <daniel.silverstone@canonical.com>52 DEBUG Recipients: ... Silverstone ...
53 ...53 ...
54 DEBUG above if files already exist in other distroseries.54 DEBUG above if files already exist in other distroseries.
55 ...55 ...
@@ -120,7 +120,7 @@
120 ...120 ...
121 DEBUG Sent a mail:121 DEBUG Sent a mail:
122 ...122 ...
123 DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>123 DEBUG Recipients: ... Bar ...
124 ...124 ...
125 DEBUG Announcing to autotest_changes@ubuntu.com125 DEBUG Announcing to autotest_changes@ubuntu.com
126 ...126 ...
@@ -137,22 +137,41 @@
137The mail 'To:' addresses contain the signer and the changer's email. The137The mail 'To:' addresses contain the signer and the changer's email. The
138announcement email contains the serieses changeslist.138announcement email contains the serieses changeslist.
139139
140 >>> [msg['To'] for msg in msgs]140 >>> def to_lower(address):
141 ['Foo Bar <foo.bar@canonical.com>,\n\tDaniel Silverstone <daniel.silverstone@canonical.com>', 'autotest_changes@ubuntu.com']141 ... """Return lower-case version of email address."""
142 ... return address.lower()
143
144 >>> def extract_addresses(header_field):
145 ... """Extract and sort addresses from an email header field."""
146 ... return sorted(
147 ... [addr.strip() for addr in header_field.split(',')],
148 ... key=to_lower)
149
150 >>> for addr in extract_addresses(msgs[0]['To']):
151 ... print addr
152 Daniel Silverstone <daniel.silverstone@canonical.com>
153 Foo Bar <foo.bar@canonical.com>
154
155 >>> print msgs[1]['To']
156 autotest_changes@ubuntu.com
142157
143The mail 'Bcc:' address is the uploader. The announcement has the uploader158The mail 'Bcc:' address is the uploader. The announcement has the uploader
144and the Debian derivatives address for the package uploaded.159and the Debian derivatives address for the package uploaded.
145160
146 >>> [msg['Bcc'] for msg in msgs]161 >>> for msg in msgs:
147 ['Root <root@localhost>', 'Root <root@localhost>, netapplet_derivatives@packages.qa.debian.org']162 ... print extract_addresses(msg['Bcc'])
163 ['Root <root@localhost>']
164 ['netapplet_derivatives@packages.qa.debian.org', 'Root <root@localhost>']
148165
149The mail 'From:' addresses are the uploader and the changer.166The mail 'From:' addresses are the uploader and the changer.
150167
151 >>> [msg['From'] for msg in msgs]168 >>> for msg in msgs:
152 ['Root <root@localhost>', 'Daniel Silverstone <daniel.silverstone@canonical.com>']169 ... print msg['From']
170 Root <root@localhost>
171 Daniel Silverstone <daniel.silverstone@canonical.com>
153172
154 >>> notification['Subject']173 >>> print notification['Subject']
155 '[ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)'174 [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
156175
157The mail body contains the same list of files again:176The mail body contains the same list of files again:
158177
@@ -188,7 +207,7 @@
188 ...207 ...
189 DEBUG Sent a mail:208 DEBUG Sent a mail:
190 ...209 ...
191 DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>210 DEBUG Recipients: ... Silverstone ...
192 ...211 ...
193 DEBUG above if files already exist in other distroseries.212 DEBUG above if files already exist in other distroseries.
194 ...213 ...
@@ -200,13 +219,15 @@
200219
201The mail headers are the same as before:220The mail headers are the same as before:
202221
203 >>> notification['To']222 >>> for addr in extract_addresses(notification['To']):
204 'Foo Bar <foo.bar@canonical.com>,\n\tDaniel Silverstone <daniel.silverstone@canonical.com>'223 ... print addr
205 >>> notification['Bcc']224 Daniel Silverstone <daniel.silverstone@canonical.com>
206 'Root <root@localhost>'225 Foo Bar <foo.bar@canonical.com>
226 >>> print notification['Bcc']
227 Root <root@localhost>
207228
208 >>> notification['Subject']229 >>> print notification['Subject']
209 '[ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)'230 [ubuntu/breezy-autotest] netapplet 0.99.6-1 (New)
210231
211The mail body contains the same list of files again:232The mail body contains the same list of files again:
212233
@@ -241,7 +262,7 @@
241 ...262 ...
242 DEBUG Subject: [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Rejected)263 DEBUG Subject: [ubuntu/breezy-autotest] netapplet 0.99.6-1 (Rejected)
243 DEBUG Sender: Root <root@localhost>264 DEBUG Sender: Root <root@localhost>
244 DEBUG Recipients: Foo Bar <foo.bar@canonical.com>, Daniel Silverstone <daniel.silverstone@canonical.com>265 DEBUG Recipients: ... Bar ...
245 DEBUG Bcc: Root <root@localhost>266 DEBUG Bcc: Root <root@localhost>
246 DEBUG Body:267 DEBUG Body:
247 DEBUG Rejected:268 DEBUG Rejected:
248269
=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py 2011-08-25 08:29:37 +0000
+++ lib/lp/soyuz/model/queue.py 2011-10-27 16:44:31 +0000
@@ -868,7 +868,7 @@
868 signer, self.sourcepackagerelease, self.builds, self.customfiles,868 signer, self.sourcepackagerelease, self.builds, self.customfiles,
869 self.archive, self.distroseries, self.pocket, summary_text,869 self.archive, self.distroseries, self.pocket, summary_text,
870 changes, changesfile_content, changes_file_object,870 changes, changesfile_content, changes_file_object,
871 status_action[self.status], dry_run, logger)871 status_action[self.status], dry_run=dry_run, logger=logger)
872872
873 def _isPersonUploader(self, person):873 def _isPersonUploader(self, person):
874 """Return True if person is an uploader to the package's distro."""874 """Return True if person is an uploader to the package's distro."""
875875
=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
--- lib/lp/soyuz/scripts/packagecopier.py 2011-09-26 06:30:07 +0000
+++ lib/lp/soyuz/scripts/packagecopier.py 2011-10-27 16:44:31 +0000
@@ -533,8 +533,8 @@
533 Verifies if each copy can be performed using `CopyChecker` and533 Verifies if each copy can be performed using `CopyChecker` and
534 raises `CannotCopy` if one or more copies could not be performed.534 raises `CannotCopy` if one or more copies could not be performed.
535535
536 When `CannotCopy`is raised call sites are in charge to rollback the536 When `CannotCopy` is raised, call sites are responsible for rolling
537 transaction or performed copies will be commited.537 back the transaction. Otherwise, performed copies will be commited.
538538
539 Wrapper for `do_direct_copy`.539 Wrapper for `do_direct_copy`.
540540
@@ -611,8 +611,7 @@
611 # In zopeless mode this email will be sent immediately.611 # In zopeless mode this email will be sent immediately.
612 notify(612 notify(
613 person, source.sourcepackagerelease, [], [], archive,613 person, source.sourcepackagerelease, [], [], archive,
614 series, pocket, summary_text=error_text,614 series, pocket, summary_text=error_text, action='rejected')
615 action='rejected')
616 raise CannotCopy(error_text)615 raise CannotCopy(error_text)
617616
618 overrides_index = 0617 overrides_index = 0
@@ -648,8 +647,7 @@
648 if send_email:647 if send_email:
649 notify(648 notify(
650 person, source.sourcepackagerelease, [], [], archive,649 person, source.sourcepackagerelease, [], [], archive,
651 destination_series, pocket, changes=None,650 destination_series, pocket, action='accepted',
652 action='accepted',
653 announce_from_person=announce_from_person,651 announce_from_person=announce_from_person,
654 previous_version=old_version)652 previous_version=old_version)
655653