← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/pocket-permissions into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/pocket-permissions into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #914779 in Launchpad itself: "Pocket maintainers cannot always upload to their pocket"
  https://bugs.launchpad.net/launchpad/+bug/914779

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/pocket-permissions/+merge/109192

== Summary ==

Bug 914779: we'd like to have per-pocket upload permissions.

== Proposed fix ==

In https://code.launchpad.net/~cjwatson/launchpad/db-pocket-permissions/+merge/106091, I added a database column to support this work, which has been deployed.  This adds the (I think) fairly obvious set of archive methods to create, delete, and query archive permissions for pockets.

== LOC Rationale ==

I hate doctests, so I went through archive.txt and archivearch.txt and variously deleted tests that duplicated existing unit tests and converted tests to unit tests when they weren't already covered there.  This was enough to bring me down to -46.

== Tests ==

bin/test -vvct soyuz.tests.test_archive

== Demo and Q/A ==

On dogfood, create a pocket upload permission (say, for -backports) using Archive.newPocketUploader for a user who doesn't otherwise have upload permission, and try to upload something.

== Lint ==

Just two false positives due to pocketlint not understanding property setters:

./lib/lp/soyuz/model/archive.py
     380: redefinition of function 'suppress_subscription_notifications' from line 371
     396: redefinition of function 'private' from line 392
-- 
https://code.launchpad.net/~cjwatson/launchpad/pocket-permissions/+merge/109192
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/pocket-permissions into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py	2012-05-14 03:12:44 +0000
+++ lib/lp/_schema_circular_imports.py	2012-06-07 17:18:24 +0000
@@ -396,9 +396,14 @@
     IArchive, 'getQueueAdminsForComponent', IArchivePermission)
 patch_collection_return_type(
     IArchive, 'getComponentsForQueueAdmin', IArchivePermission)
+patch_collection_return_type(
+    IArchive, 'getPocketsForUploader', IArchivePermission)
+patch_collection_return_type(
+    IArchive, 'getUploadersForPocket', IArchivePermission)
 patch_entry_return_type(IArchive, 'newPackageUploader', IArchivePermission)
 patch_entry_return_type(IArchive, 'newPackagesetUploader', IArchivePermission)
 patch_entry_return_type(IArchive, 'newComponentUploader', IArchivePermission)
+patch_entry_return_type(IArchive, 'newPocketUploader', IArchivePermission)
 patch_entry_return_type(IArchive, 'newQueueAdmin', IArchivePermission)
 patch_plain_parameter_type(IArchive, 'syncSources', 'from_archive', IArchive)
 patch_plain_parameter_type(IArchive, 'syncSource', 'from_archive', IArchive)
@@ -432,6 +437,12 @@
     IArchive, '_checkUpload', 'distroseries', IDistroSeries)
 patch_choice_parameter_type(
     IArchive, '_checkUpload', 'pocket', PackagePublishingPocket)
+patch_choice_parameter_type(
+    IArchive, 'getUploadersForPocket', 'pocket', PackagePublishingPocket)
+patch_choice_parameter_type(
+    IArchive, 'newPocketUploader', 'pocket', PackagePublishingPocket)
+patch_choice_parameter_type(
+    IArchive, 'deletePocketUploader', 'pocket', PackagePublishingPocket)
 patch_plain_parameter_type(
     IArchive, 'newPackagesetUploader', 'packageset', IPackageset)
 patch_plain_parameter_type(

=== modified file 'lib/lp/code/tests/test_branch.py'
--- lib/lp/code/tests/test_branch.py	2012-05-31 03:54:13 +0000
+++ lib/lp/code/tests/test_branch.py	2012-06-07 17:18:24 +0000
@@ -274,25 +274,7 @@
             None,
             archive.verifyUpload(
                 person, spn, component, distroseries,
-                strict_component))
-
-    def assertCannotUpload(
-        self, reason, person, spn, archive, component, distroseries=None):
-        """Assert that 'person' cannot upload to the archive.
-
-        :param reason: The expected reason for not being able to upload. A
-            string.
-        :param person: The person trying to upload.
-        :param spn: The `ISourcePackageName` being uploaded to. None if the
-            package does not yet exist.
-        :param archive: The `IArchive` being uploaded to.
-        :param component: The IComponent to which the package belongs.
-        """
-        if distroseries is None:
-            distroseries = archive.distribution.currentseries
-        exception = archive.verifyUpload(
-            person, spn, component, distroseries)
-        self.assertEqual(reason, str(exception))
+                strict_component=strict_component))
 
     def test_package_upload_permissions_grant_branch_edit(self):
         # If you can upload to the package, then you are also allowed to write

=== modified file 'lib/lp/soyuz/doc/archive.txt'
--- lib/lp/soyuz/doc/archive.txt	2012-05-23 05:42:24 +0000
+++ lib/lp/soyuz/doc/archive.txt	2012-06-07 17:18:24 +0000
@@ -64,23 +64,16 @@
     >>> cprov_archive.failed_count
     1
 
-relative_build_score and external_dependencies are properties that can be set
-only by LP admins and read by anyone.
-
-relative_build_score is a signed integer that represents a delta to all the
-build scores for builds done in the archive.  The default value is zero:
-
-    >>> cprov_archive.relative_build_score
-    0
-
-Amending it as an unprivileged user results in failure:
-
-    >>> cprov_archive.relative_build_score = 100
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (..., 'relative_build_score', 'launchpad.Moderate')
-
-external_dependencies is a text field that should contain a comma-separated
+    >>> cprov_private_ppa = factory.makeArchive(
+    ...     owner=cprov, name='myprivateppa',
+    ...     distribution=cprov_archive.distribution)
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> cprov_private_ppa.buildd_secret = 'really secret'
+    >>> cprov_private_ppa.private = True
+    >>> login(ANONYMOUS)
+
+external_dependencies is a property that can be set only by LP admins and
+read by anyone.  It is a text field that should contain a comma-separated
 list of sources.list entries in the format:
 deb http[s]://[user:pass@]<host>[/path] %(series)s[-pocket] [components]
 where the series variable is replaced with the series name of the context
@@ -97,69 +90,11 @@
     ...
     Unauthorized: (..., 'external_dependencies', 'launchpad.Commercial')
 
-As a Launchpad admin, setting these properties will work.
+As a Launchpad admin, setting this property will work.
 
     >>> login("admin@xxxxxxxxxxxxx")
-    >>> cprov_archive.relative_build_score = 100
     >>> cprov_archive.external_dependencies = "deb http://foo hardy bar"
 
-The buildd_secret is used by the slave scanner when generating a
-sources.list entry for the builder to access a private archive.  It is
-essentially the password to the archive for the builder.
-
-It can only be set by users with launchpad.Commercial permission in the
-archive, i.e. an admin or a commercial admin. We create a new PPA for
-cprov here as changing privacy of a PPA is not allowed when sources have
-already been published.
-
-    >>> cprov_private_ppa = factory.makeArchive(
-    ...     owner=cprov, name='myprivateppa',
-    ...     distribution=cprov_archive.distribution)
-    >>> login(ANONYMOUS)
-    >>> cprov_private_ppa.buildd_secret = 'boing'
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (..., 'buildd_secret', 'launchpad.Commercial')
-
-Commercial Member, a commercial admin but not an admin, can set
-'buildd_secret'.
-
-    >>> login("commercial-member@xxxxxxxxxxxxx")
-    >>> cprov_private_ppa.buildd_secret = 'not so secret at all'
-
-Foo Bar, an admin, can set 'buildd_secret'.
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> cprov_archive.buildd_secret = 'not so secret'
-
-In a public PPA, 'buildd_secret' still visible to anyone.
-
-    >>> login(ANONYMOUS)
-    >>> print cprov_archive.private
-    False
-
-    >>> print cprov_archive.buildd_secret
-    not so secret
-
-Once made private, 'buildd_secret' content can only be read by users with
-'launchpad.View' in the archive.
-
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> cprov_private_ppa.buildd_secret = 'really secret'
-    >>> cprov_private_ppa.private = True
-
-    >>> login(ANONYMOUS)
-    >>> print cprov_private_ppa.buildd_secret
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (..., 'buildd_secret', 'launchpad.View')
-
-Celso can read 'buildd_secret' contents for his PPA.
-
-    >>> login('celso.providelo@xxxxxxxxxxxxx')
-    >>> print cprov_private_ppa.buildd_secret
-    really secret
-
 Useful properties:
 
     >>> print cprov_archive.archive_url
@@ -543,160 +478,6 @@
     >>> status_lookup.count()
     2
 
-getBuildCounters
-----------------
-
-IArchive.getBuildCounters() allows callsites to quickly present
-the number of builds in a pre-defined status for a given IArchive.
-
-    >>> def print_build_counters(build_counters):
-    ...     sorted_keys = sorted(build_counters)
-    ...     for key in sorted_keys:
-    ...         print key, build_counters[key]
-
-Build counters for Celso's PPA.
-
-    >>> print_build_counters(cprov_archive.getBuildCounters())
-    failed     1
-    pending    0
-    succeeded  3
-    superseded 0
-    total      4
-
-Build counters for ubuntu primary archive.
-
-    >>> print_build_counters(ubuntu.main_archive.getBuildCounters())
-    failed     5
-    pending    2
-    succeeded  8
-    superseded 3
-    total      18
-
-An option argument can be used to exclude any builds that have the status
-`NEEDSBUILD`:
-
-    >>> print_build_counters(
-    ...     ubuntu.main_archive.getBuildCounters(include_needsbuild=False))
-    failed 5
-    pending 1
-    succeeded 8
-    superseded 3
-    total 17
-
-
-getBuildSummariesForSourceIds()
--------------------------------
-
-IArchive.getBuildSummariesForSourceIds() allows callsites to get an update
-on the build statuses for a set of source publishing record ids. This
-is useful for dynamically updating a page which displays a small batch of
-source packages, such as the PPA/Archive pages.
-
-Create a small function for displaying the results:
-
-    >>> def print_build_summary(summary):
-    ...     print "%s\n%s\nRelevant builds:\n%s" % (
-    ...         summary['status'].title,
-    ...         summary['status'].description,
-    ...         "\n".join(
-    ...             " - %s" % build.title for build in summary['builds'])
-    ...     )
-
-    >>> def print_build_summaries(summaries):
-    ...     for source_id, summary in sorted(summaries.items()):
-    ...         print "Source ID: %s" % source_id
-    ...         print_build_summary(summary)
-
-Now print the build summaries for firefox and foo_bar:
-
-    >>> firefox_source = ubuntu.getSourcePackage('mozilla-firefox')
-    >>> firefox_source_pub = firefox_source.publishing_history[0]
-    >>> foobar = ubuntu.getSourcePackage('foobar')
-    >>> foo_pub = foobar.publishing_history[0]
-
-    >>> build_summaries = \
-    ...     ubuntu.main_archive.getBuildSummariesForSourceIds(
-    ...         [firefox_source_pub.id, foo_pub.id])
-    >>> print_build_summaries(build_summaries)
-    Source ID: 18
-    FULLYBUILT
-    All builds were built successfully.
-    Relevant builds:
-     - hppa build of mozilla-firefox 0.9 in ubuntu warty RELEASE
-     - i386 build of mozilla-firefox 0.9 in ubuntu warty RELEASE
-    Source ID: 22
-    FAILEDTOBUILD
-    There were build failures.
-    Relevant builds:
-     - i386 build of foobar 1.0 in ubuntu warty RELEASE
-
-
-No public access to IArchiveView methods
-----------------------------------------
-
-Both the getBuildCounters() and getBuildSummariesForSourceIds() methods are
-not publically available, but available only to users who have permission to
-view the archive:
-
-    # First we create some source/binary packages in cprov's private
-    # PPA so that we'll have some results to view.
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
-    >>> test_publisher = SoyuzTestPublisher()
-    >>> ignore = test_publisher.setUpDefaultDistroSeries(warty)
-    >>> test_publisher.addFakeChroots(warty)
-    >>> ignore = test_publisher.getPubBinaries(archive=cprov_private_ppa)
-
-    # Grab some source IDs from the archive that we can use for calls to
-    # getBuildSummariesForSourceIds():
-    >>> source_ids = [cprov_private_ppa.getPublishedSources()[0].id]
-
-Then verify that an admin can see the counters and build summaries:
-
-    >>> print_build_counters(cprov_private_ppa.getBuildCounters())
-    failed     0
-    ...
-    succeeded  1
-    ...
-    total      1
-    >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
-    ...     source_ids))
-    Source ID:...
-    FULLYBUILT_PENDING
-    All builds were built successfully but have not yet been published.
-    Relevant builds:
-      - i386 build of foo 666 in ubuntu warty RELEASE
-
-Next veryify that cprov can still access the build counters:
-
-    >>> login('celso.providelo@xxxxxxxxxxxxx')
-    >>> print_build_counters(cprov_private_ppa.getBuildCounters())
-    failed     0
-    ...
-    succeeded  1
-    ...
-    total      1
-    >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
-    ...     source_ids))
-    Source ID:...
-    ...
-      - i386 build of foo 666 in ubuntu warty RELEASE
-
-But the public cannot:
-
-    >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> print_build_counters(cprov_private_ppa.getBuildCounters())
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (..., 'getBuildCounters', 'launchpad.View')
-    >>> print_build_summaries(cprov_private_ppa.getBuildSummariesForSourceIds(
-    ...     source_ids))
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (..., 'getBuildSummariesForSourceIds', 'launchpad.View')
-
-
 Package Counters
 ----------------
 
@@ -806,26 +587,6 @@
     2
 
 
-Getting an Archive's source-package releases
---------------------------------------------
-
-The method getSourcePackageReleases() is provided to return the unique
-source-package releases for the archive. By default, all releases will
-be returned, but you can also ask for releases with builds in a certain
-state.
-
-    >>> from lp.buildmaster.enums import BuildStatus
-    >>> releases = cprov_archive.getSourcePackageReleases(
-    ...     build_status=BuildStatus.FULLYBUILT)
-    >>> for release in releases:
-    ...     print release.title
-    mozilla-firefox - 0.9
-    pmount - 0.1-1
-    iceweasel - 1.0
-
-For further details see the `TestGetSourcePackageReleases` unit-test.
-
-
 Sources available for deletions
 -------------------------------
 
@@ -932,6 +693,7 @@
 
 Or return build records in a specific status:
 
+    >>> from lp.buildmaster.enums import BuildStatus
     >>> cprov_archive.getBuildRecords(
     ...     build_state=BuildStatus.FULLYBUILT).count()
     3
@@ -1963,24 +1725,6 @@
     ultimate-copy    copy-owner1   False    True
 
 
-Getting publishing records across a set of Archives
----------------------------------------------------
-
-The IArchiveSet utility provides a getPublicationsInArchives() method
-that can be used to find the current publishing records for a source
-package in the provided list of archives for a specific distribution.
-
-    >>> pubs = archive_set.getPublicationsInArchives(
-    ...     firefox_source_pub.sourcepackagerelease.sourcepackagename,
-    ...     [ubuntu.main_archive],
-    ...     firefox_source_pub.distroseries.distribution)
-    >>> for pub in pubs:
-    ...     print "%s - %s in %s" % (
-    ...         pub.source_package_name,
-    ...         pub.source_package_version,
-    ...         pub.archive.displayname)
-    mozilla-firefox - 0.9 in Primary Archive for Ubuntu Linux
-
 Archive Permission Checking
 ---------------------------
 
@@ -2257,6 +2001,9 @@
 First we use the SoyuzTestPublisher to make some test publications in
 hoary:
 
+    >>> from lp.soyuz.tests.test_publishing import (
+    ...     SoyuzTestPublisher)
+    >>> test_publisher = SoyuzTestPublisher()
     >>> test_publisher.addFakeChroots(hoary)
     >>> unused = test_publisher.setUpDefaultDistroSeries(hoary)
     >>> discard = test_publisher.getPubSource(
@@ -2482,281 +2229,3 @@
     >>> copy = mark.archive.getPublishedSources(name="overridden").one()
     >>> print copy.section.name
     python
-
-
-Publish flag
-------------
-
-Every archive has a "publish" flag that governs whether it should be
-published or not. Upon creation that flag is false for copy archives but
-true for all other archive types.
-
-    >>> uber = getUtility(IDistributionSet).new(
-    ... 'uberdistro', 'The uberdistro', 'The mother of all distros',
-    ... 'All you would want from a distro', 'zero', 'uberdistro.org',
-    ... mark, cprov, cprov)
-
-The primary archive for the Überdistro was created by the
-IDistributionSet.new() method. Let's check its publish flag.
-
-    >>> uber_primary = getUtility(IArchiveSet).getByDistroPurpose(
-    ...     uber, ArchivePurpose.PRIMARY)
-    >>> uber_primary.publish
-    True
-
-    >>> uber_partner = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.PARTNER,
-    ...     distribution=uber, name='uber-partner')
-    >>> uber_partner.publish
-    True
-
-The 'sandbox archive' is a PPA that was newly created above.
-
-    >>> sandbox_archive.is_ppa
-    True
-    >>> sandbox_archive.publish
-    True
-
-    >>> uber_copy = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.COPY,
-    ...     distribution=uber, name='uber-copy')
-    >>> uber_copy.publish
-    False
-
-
-The name uniqueness constraints for archives
---------------------------------------------
-
-The names of archives other than PPAs must be unique for a given
-distribution. Trying to create an archive with the same name and
-distribution but with a different owner will fail.
-
-    >>> copycat_archive = getUtility(IArchiveSet).new(
-    ...     owner=mark, purpose=ArchivePurpose.COPY,
-    ...     distribution=uber, name='uber-copy')
-    Traceback (most recent call last):
-    ...
-    AssertionError: archive 'uber-copy' exists ... in 'uberdistro'.
-
-The same constraint is enforced for other archive types e.g. for partner
-archives.
-
-    >>> copycat_archive = getUtility(IArchiveSet).new(
-    ...     owner=mark, purpose=ArchivePurpose.PARTNER,
-    ...     distribution=uber, name='uber-partner')
-    Traceback (most recent call last):
-    ...
-    AssertionError: archive 'uber-partner' exists ... in 'uberdistro'.
-
-The names of PPAs must be unique per owner and distribution.
-
-    >>> print mark.archive.displayname
-    PPA for Mark Shuttleworth
-
-    >>> print mark.archive.name
-    ppa
-
-    >>> dup_ppa = getUtility(IArchiveSet).new(
-    ...     owner=mark, purpose=ArchivePurpose.PPA,
-    ...     distribution=ubuntu, name='ppa')
-    Traceback (most recent call last):
-    ...
-    AssertionError: Person 'mark' already has a PPA named 'ppa'.
-
-While multiple PPAs per user isn't yet fully suported we may create
-other PPAs, but they won't affect the existing traversal from IPerson
-to a single IArchive.
-
-    >>> another_ppa = getUtility(IArchiveSet).new(
-    ...     owner=mark, purpose=ArchivePurpose.PPA,
-    ...     distribution=ubuntu, name='nightly')
-
-    >>> print another_ppa.owner.displayname
-    Mark Shuttleworth
-
-    >>> print another_ppa.name
-    nightly
-
-`IPerson.archive` is still pointing to the PPA named 'ppa'.
-
-    >>> print mark.archive.displayname
-    PPA for Mark Shuttleworth
-
-    >>> print mark.archive.name
-    ppa
-
-The ppas named differently than the default ('ppa') have a slightly
-different displayname format, including their names.
-
-    >>> print another_ppa.displayname
-    PPA named nightly for Mark Shuttleworth
-
-Additionally, archives, despite of their purpose, cannot have the same
-name as their distribution.
-
-    >>> boingolinux = factory.makeDistribution(name='boingolinux')
-
-    >>> getUtility(IArchiveSet).new(
-    ...     owner=mark, purpose=ArchivePurpose.PRIMARY,
-    ...     distribution=boingolinux, name=boingolinux.name)
-    Traceback (most recent call last):
-    ...
-    AssertionError: Archives cannot have the same name as their
-    distribution.
-
-
-Looking up named PPAs
----------------------
-
-Additionally to the locked 'archive' property, `IPerson` also offers
-`ppas` property and `getPPAByName` method.
-
-`IPerson.ppas` returns a list with all PPA owned by the context person
-or team ordered by name.
-
-    >>> for ppa in mark.ppas:
-    ...     print ppa.name
-    nightly
-    ppa
-
-`IPerson.getPPAByName` allows call sites to look up PPAs owned by the
-context person with a given name (exact match).
-
-    >>> default_ppa = mark.getPPAByName('ppa')
-    >>> default_ppa == mark.archive
-    True
-
-    >>> nightly_ppa = mark.getPPAByName('nightly')
-    >>> another_ppa == nightly_ppa
-    True
-
-When a suitable PPA couldn't be found, NoSuchPPA is raised.
-
-    >>> print mark.getPPAByName('not-found')
-    Traceback (most recent call last):
-    ...
-    NoSuchPPA: No such ppa: 'not-found'.
-
-
-Editable displayname
---------------------
-
-If 'displayname' is omitted on archive created, a default form is
-automatically used.
-    >>> new_ppa = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.PPA,
-    ...     distribution=ubuntu, name='test-ppa')
-    >>> print new_ppa.displayname
-    PPA named test-ppa for Celso Providelo
-
-When provided 'displayname' is used as given.
-
-    >>> new_copy = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.COPY,
-    ...     displayname='Rock and roll with rebuilds!',
-    ...     distribution=ubuntu, name='test-rebuild')
-    >>> print new_copy.displayname
-    Rock and roll with rebuilds!
-
-After archive creation, the 'displayname' can be edited by the archive
-anyone with 'edit' permissions on the archive.
-
-    >>> login("no-priv@xxxxxxxxxxxxx")
-    >>> new_ppa.displayname = 'No-way!'
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (<Archive at ...>, 'displayname', 'launchpad.Edit')
-
-    >>> login('celso.providelo@xxxxxxxxxxxxx')
-    >>> new_ppa.displayname = 'My testing packages for jaunty'
-
-
-Signing-key propagation
------------------------
-
-Signing keys are, by default, shared between PPAs owned by the same
-user/team.
-
-Celso's default PPA currently has no signing-key.
-
-    >>> print cprov.archive.signing_key
-    None
-
-When a named-ppa is created there is no key to be shared, this case
-is worked out when generating new signing key. See archive-signing.txt
-for more information.
-
-    >>> no_key_ppa = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.PPA,
-    ...     distribution=ubuntu, name='no-key')
-
-    >>> print no_key_ppa.signing_key
-    None
-
-We will select the only available IGPGKey from the sampledata.
-
-    >>> foo_bar = getUtility(IPersonSet).getByName('name16')
-    >>> [a_key] = foo_bar.gpg_keys
-    >>> print a_key.displayname
-    1024D/12345678
-
-And use it as the Celso's default PPA signing key.
-
-    >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> cprov.archive.signing_key = a_key
-    >>> login('celso.providelo@xxxxxxxxxxxxx')
-
-Now there is a signing-key to be propagated and a new named-ppa is
-already created accordingly.
-
-    >>> ppa_with_key = getUtility(IArchiveSet).new(
-    ...     owner=cprov, purpose=ArchivePurpose.PPA,
-    ...     distribution=ubuntu, name='has-key')
-
-    >>> ppa_with_key.signing_key == cprov.archive.signing_key
-    True
-
-
-Download counts
----------------
-
-Counts of downloads per binary package release, day and country are kept
-up to date by a log-processing script. Archives have a method to get the
-total number of downloads for a particular binary package release.
-
-    >>> login('mark@xxxxxxxxxxx')
-    >>> binaries = test_publisher.getPubBinaries(
-    ...     architecturespecific=True)
-    >>> archive = binaries[0].archive
-    >>> binary0, binary1 = (b.binarypackagerelease for b in binaries)
-
-The new packages have no downloads yet.
-
-    >>> print archive.getPackageDownloadTotal(binary0)
-    0
-    >>> print archive.getPackageDownloadTotal(binary1)
-    0
-
-We will fake some package downloads.
-
-    >>> from datetime import date
-    >>> from lp.services.worlddata.interfaces.country import ICountrySet
-    >>> australia = getUtility(ICountrySet)['AU']
-    >>> uk = getUtility(ICountrySet)['GB']
-
-    >>> archive.updatePackageDownloadCount(
-    ...     binary0, date(2010, 2, 21), None, 10)
-    >>> archive.updatePackageDownloadCount(
-    ...     binary0, date(2010, 2, 22), uk, 5)
-    >>> archive.updatePackageDownloadCount(
-    ...     binary0, date(2010, 2, 22), australia, 4)
-
-    >>> archive.updatePackageDownloadCount(
-    ...     binary1, date(2010, 2, 21), australia, 2)
-    >>> archive.updatePackageDownloadCount(
-    ...     binary1, date(2010, 2, 21), uk, 1)
-
-    >>> print archive.getPackageDownloadTotal(binary0)
-    19
-    >>> print archive.getPackageDownloadTotal(binary1)
-    3

=== removed file 'lib/lp/soyuz/doc/archivearch.txt'
--- lib/lp/soyuz/doc/archivearch.txt	2010-08-23 16:51:11 +0000
+++ lib/lp/soyuz/doc/archivearch.txt	1970-01-01 00:00:00 +0000
@@ -1,72 +0,0 @@
-The `ArchiveArch` table facilitates the association of archives and
-processor families. This allows a user to specify (or limit) what
-processors the source packages in a certain archives will be built
-for.
-
-    >>> from lp.soyuz.enums import ArchivePurpose
-    >>> rebuild_archive = factory.makeArchive(
-    ...     purpose=ArchivePurpose.COPY, name='archivearch-test')
-
-The rebuild archive has no associated processor families yet.
-
-    >>> from lp.soyuz.interfaces.archivearch import IArchiveArchSet
-    >>> aa_set = getUtility(IArchiveArchSet)
-    >>> rset = aa_set.getByArchive(rebuild_archive)
-    >>> print rset.count()
-    0
-
-The utility allows us to associate archives with processor families
-and we'll tie the rebuild archive to the 'amd64' processor family.
-
-    # Retrieve the 'amd64' and 'x86' processor families available
-    # in the sampledata.
-    >>> from lp.soyuz.interfaces.processor import IProcessorFamilySet
-    >>> amd64 = getUtility(IProcessorFamilySet).getByName('amd64')
-    >>> x86 = getUtility(IProcessorFamilySet).getByName('x86')
-
-    >>> ignore = aa_set.new(rebuild_archive, amd64)
-
-Now we have an association between the rebuild archive to the 'amd64'
-processor family.
-
-    >>> archive_arches = aa_set.getByArchive(rebuild_archive)
-    >>> archive_arches.count()
-    1
-
-    >>> [archive_arch] = list(archive_arches)
-    >>> print archive_arch.archive.name
-    archivearch-test
-
-    >>> print archive_arch.processorfamily.name
-    amd64
-
-Let's add another association for 'x86' processor family.
-
-    >>> ignore = aa_set.new(rebuild_archive, x86)
-
-    >>> archive_arches = aa_set.getByArchive(rebuild_archive)
-    >>> print archive_arches.count()
-    2
-
-The result follows the creation order, so the just-created
-`ArchiveArch` comes last.
-
-    >>> [old, x86_archive_arch] = list(archive_arches)
-    >>> print x86_archive_arch.archive.name
-    archivearch-test
-
-    >>> print x86_archive_arch.processorfamily.name
-    x86
-
-Last but not least, we query for a specific association.
-
-    >>> archive_arches = aa_set.getByArchive(rebuild_archive, amd64)
-    >>> archive_arches.count()
-    1
-
-    >>> [amd64_archive_arch] = list(archive_arches)
-    >>> print amd64_archive_arch.archive.name
-    archivearch-test
-
-    >>> print amd64_archive_arch.processorfamily.name
-    amd64

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2012-05-29 15:54:57 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2012-06-07 17:18:24 +0000
@@ -626,7 +626,7 @@
         """
 
     def verifyUpload(person, sourcepackagename, component,
-                      distroseries, strict_component=True):
+                      distroseries, strict_component=True, pocket=None):
         """Can 'person' upload 'sourcepackagename' to this archive ?
 
         :param person: The `IPerson` trying to upload to the package. Referred
@@ -639,6 +639,8 @@
         :param strict_component: True if access to the specific component for
             the package is needed to upload to it. If False, then access to
             any component will do.
+        :param pocket: The `PackagePublishingPocket` being uploaded to. If
+            None, then pocket permissions are not checked.
         :return: CannotUploadToArchive if 'person' cannot upload to the
             archive,
             None otherwise.
@@ -762,6 +764,21 @@
         """
 
     @operation_parameters(
+        person=Reference(schema=IPerson))
+    # Really IArchivePermission, set in _schema_circular_imports to avoid
+    # circular import.
+    @operation_returns_collection_of(Interface)
+    @export_read_operation()
+    @operation_for_version("devel")
+    def getPocketsForUploader(person):
+        """Return the pockets that 'person' can upload to this archive.
+
+        :param person: An `IPerson` wishing to upload to an archive.
+        :return: A `set` of `PackagePublishingPocket` items that 'person'
+            can upload to.
+        """
+
+    @operation_parameters(
         sourcepackagename=TextLine(
             title=_("Source package name"), required=True),
         person=Reference(schema=IPerson))
@@ -1173,6 +1190,24 @@
         :return: A list of `IArchivePermission` records.
         """
 
+    @operation_parameters(
+        pocket=Choice(
+            title=_("Pocket"),
+            # Really PackagePublishingPocket, circular import fixed below.
+            vocabulary=DBEnumeratedType,
+            required=True),
+        )
+    # Really IArchivePermission, set below to avoid circular import.
+    @operation_returns_collection_of(Interface)
+    @export_read_operation()
+    @operation_for_version("devel")
+    def getUploadersForPocket(pocket):
+        """Return `IArchivePermission` records for the pocket's uploaders.
+
+        :param pocket: A `PackagePublishingPocket`.
+        :return: A list of `IArchivePermission` records.
+        """
+
     def hasAnyPermission(person):
         """Whether or not this person has any permission at all on this
         archive.
@@ -1507,6 +1542,30 @@
 
     @operation_parameters(
         person=Reference(schema=IPerson),
+        pocket=Choice(
+            title=_("Pocket"),
+            # Really PackagePublishingPocket, circular import fixed below.
+            vocabulary=DBEnumeratedType,
+            required=True),
+        )
+    # Really IArchivePermission, set below to avoid circular import.
+    @export_factory_operation(Interface, [])
+    @operation_for_version("devel")
+    def newPocketUploader(person, pocket):
+        """Add permission for a person to upload to a pocket.
+
+        :param person: An `IPerson` whom should be given permission.
+        :param component: A `PackagePublishingPocket`.
+        :return: An `IArchivePermission` which is the newly-created
+            permission.
+        :raises InvalidPocketForPartnerArchive: if this archive is a partner
+            archive and the pocket is not RELEASE or PROPOSED.
+        :raises InvalidPocketForPPA: if this archive is a PPA and the pocket
+            is not RELEASE.
+        """
+
+    @operation_parameters(
+        person=Reference(schema=IPerson),
         component_name=TextLine(
             title=_("Component Name"), required=True))
     # Really IArchivePermission, set below to avoid circular import.
@@ -1573,6 +1632,23 @@
 
     @operation_parameters(
         person=Reference(schema=IPerson),
+        pocket=Choice(
+            title=_("Pocket"),
+            # Really PackagePublishingPocket, circular import fixed below.
+            vocabulary=DBEnumeratedType,
+            required=True),
+        )
+    @export_write_operation()
+    @operation_for_version("devel")
+    def deletePocketUploader(person, pocket):
+        """Revoke permission for the person to upload to the pocket.
+
+        :param person: An `IPerson` whose permission should be revoked.
+        :param pocket: A `PackagePublishingPocket`.
+        """
+
+    @operation_parameters(
+        person=Reference(schema=IPerson),
         component_name=TextLine(
             title=_("Component Name"), required=True))
     @export_write_operation()

=== modified file 'lib/lp/soyuz/interfaces/archivepermission.py'
--- lib/lp/soyuz/interfaces/archivepermission.py	2011-12-24 16:54:44 +0000
+++ lib/lp/soyuz/interfaces/archivepermission.py	2012-06-07 17:18:24 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0213
@@ -31,6 +31,7 @@
     )
 
 from lp import _
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.sourcepackagename import ISourcePackageName
 from lp.services.fields import PublicPersonChoice
 from lp.soyuz.enums import ArchivePermissionType
@@ -117,6 +118,13 @@
                 "package set."),
             required=True))
 
+    pocket = exported(
+        Choice(
+            title=_("Pocket"),
+            description=_("The pocket that this permission is for."),
+            vocabulary=PackagePublishingPocket,
+            required=True))
+
 
 class IArchiveUploader(IArchivePermission):
     """Marker interface for URL traversal of uploader permissions."""
@@ -305,6 +313,27 @@
             authorized to upload to the named source package set.
         """
 
+    def uploadersForPocket(archive, pocket):
+        """The `ArchivePermission` records for authorised pocket uploaders.
+
+        :param archive: The context `IArchive` for the permission check.
+        :param pocket: A `PackagePublishingPocket`.
+
+        :return: `ArchivePermission` records for all the uploaders who
+            are authorised for the supplied pocket.
+        """
+
+    def pocketsForUploader(archive, person):
+        """The `ArchivePermission` records for the person's upload pockets.
+
+        :param archive: The context `IArchive` for the permission check.
+        :param person: An `IPerson` for whom you want to find out which
+            pockets he has access to.
+
+        :return: `ArchivePermission` records for all the pockets that
+            'person' is allowed to upload to.
+        """
+
     def queueAdminsForComponent(archive, component):
         """The `ArchivePermission` records for authorised queue admins.
 
@@ -370,6 +399,17 @@
             already exists.
         """
 
+    def newPocketUploader(archive, person, pocket):
+        """Create and return a new `ArchivePermission` for an uploader.
+
+        :param archive: The context `IArchive` for the permission check.
+        :param person: An `IPerson` for whom you want to add permission.
+        :param component: A `PackagePublishingPocket`.
+
+        :return: The new `ArchivePermission`, or the existing one if it
+            already exists.
+        """
+
     def newQueueAdmin(archive, person, component):
         """Create and return a new `ArchivePermission` for a queue admin.
 
@@ -411,6 +451,14 @@
         :param component: An `IComponent` or a string package name.
         """
 
+    def deletePocketUploader(archive, person, pocket):
+        """Revoke upload permissions for a person.
+
+        :param archive: The context `IArchive` for the permission check.
+        :param person: An `IPerson` for whom you want to revoke permission.
+        :param pocket: A `PackagePublishingPocket`.
+        """
+
     def deleteQueueAdmin(archive, person, component):
         """Revoke queue admin permissions for a person.
 

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2012-05-29 15:54:57 +0000
+++ lib/lp/soyuz/model/archive.py	2012-06-07 17:18:24 +0000
@@ -1122,6 +1122,11 @@
         permission_set = getUtility(IArchivePermissionSet)
         return permission_set.uploadersForComponent(self, component_name)
 
+    def getUploadersForPocket(self, pocket):
+        """See `IArchive`."""
+        permission_set = getUtility(IArchivePermissionSet)
+        return permission_set.uploadersForPocket(self, pocket)
+
     def getQueueAdminsForComponent(self, component_name):
         """See `IArchive`."""
         permission_set = getUtility(IArchivePermissionSet)
@@ -1230,7 +1235,7 @@
             source_ids,
             archive=self)
 
-    def checkArchivePermission(self, user, component_or_package=None):
+    def checkArchivePermission(self, user, item=None):
         """See `IArchive`."""
         # PPA access is immediately granted if the user is in the PPA
         # team.
@@ -1245,7 +1250,7 @@
                 # interface will no longer require them because we can
                 # then relax the database constraint on
                 # ArchivePermission.
-                component_or_package = self.default_component
+                item = self.default_component
 
         # Flatly refuse uploads to copy archives, at least for now.
         if self.is_copy:
@@ -1253,8 +1258,7 @@
 
         # Otherwise any archive, including PPAs, uses the standard
         # ArchivePermission entries.
-        return self._authenticate(
-            user, component_or_package, ArchivePermissionType.UPLOAD)
+        return self._authenticate(user, item, ArchivePermissionType.UPLOAD)
 
     def canUploadSuiteSourcePackage(self, person, suitesourcepackage):
         """See `IArchive`."""
@@ -1317,10 +1321,10 @@
             return reason
         return self.verifyUpload(
             person, sourcepackagename, component, distroseries,
-            strict_component)
+            strict_component=strict_component, pocket=pocket)
 
     def verifyUpload(self, person, sourcepackagename, component,
-                     distroseries, strict_component=True):
+                     distroseries, strict_component=True, pocket=None):
         """See `IArchive`."""
         if not self.enabled:
             return ArchiveDisabled(self.displayname)
@@ -1332,6 +1336,11 @@
             else:
                 return None
 
+        # Users with pocket upload permissions may upload to anything in the
+        # given pocket.
+        if pocket is not None and self.checkArchivePermission(person, pocket):
+            return None
+
         if sourcepackagename is not None:
             # Check whether user may upload because they hold a permission for
             #   - the given source package directly
@@ -1362,9 +1371,9 @@
         return self._authenticate(
             user, component, ArchivePermissionType.QUEUE_ADMIN)
 
-    def _authenticate(self, user, component, permission):
+    def _authenticate(self, user, item, permission):
         """Private helper method to check permissions."""
-        permissions = self.getPermissions(user, component, permission)
+        permissions = self.getPermissions(user, item, permission)
         return bool(permissions)
 
     def newPackageUploader(self, person, source_package_name):
@@ -1398,6 +1407,19 @@
         return permission_set.newComponentUploader(
             self, person, component_name)
 
+    def newPocketUploader(self, person, pocket):
+        if self.is_partner:
+            if pocket not in (
+                PackagePublishingPocket.RELEASE,
+                PackagePublishingPocket.PROPOSED):
+                raise InvalidPocketForPartnerArchive()
+        elif self.is_ppa:
+            if pocket != PackagePublishingPocket.RELEASE:
+                raise InvalidPocketForPPA()
+
+        permission_set = getUtility(IArchivePermissionSet)
+        return permission_set.newPocketUploader(self, person, pocket)
+
     def newQueueAdmin(self, person, component_name):
         """See `IArchive`."""
         permission_set = getUtility(IArchivePermissionSet)
@@ -1415,6 +1437,11 @@
         return permission_set.deleteComponentUploader(
             self, person, component_name)
 
+    def deletePocketUploader(self, person, pocket):
+        """See `IArchive`."""
+        permission_set = getUtility(IArchivePermissionSet)
+        return permission_set.deletePocketUploader(self, person, pocket)
+
     def deleteQueueAdmin(self, person, component_name):
         """See `IArchive`."""
         permission_set = getUtility(IArchivePermissionSet)
@@ -1437,6 +1464,11 @@
         permission_set = getUtility(IArchivePermissionSet)
         return permission_set.componentsForUploader(self, person)
 
+    def getPocketsForUploader(self, person):
+        """See `IArchive`."""
+        permission_set = getUtility(IArchivePermissionSet)
+        return permission_set.pocketsForUploader(self, person)
+
     def getPackagesetsForUploader(self, person):
         """See `IArchive`."""
         permission_set = getUtility(IArchivePermissionSet)

=== modified file 'lib/lp/soyuz/model/archivepermission.py'
--- lib/lp/soyuz/model/archivepermission.py	2011-12-30 06:14:56 +0000
+++ lib/lp/soyuz/model/archivepermission.py	2012-06-07 17:18:24 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Database class for table ArchivePermission."""
@@ -10,6 +10,7 @@
     'ArchivePermissionSet',
     ]
 
+from lazr.enum import DBItem
 from sqlobject import (
     BoolCol,
     ForeignKey,
@@ -25,9 +26,11 @@
     alsoProvides,
     implements,
     )
+from zope.security.proxy import isinstance as zope_isinstance
 
 from lp.app.errors import NotFoundError
 from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.sourcepackagename import (
     ISourcePackageName,
     ISourcePackageNameSet,
@@ -101,6 +104,8 @@
 
     explicit = BoolCol(dbName='explicit', notNull=True, default=False)
 
+    pocket = EnumCol(dbName="pocket", schema=PackagePublishingPocket)
+
     def _init(self, *args, **kw):
         """Provide the right interface for URL traversal."""
         SQLBase._init(self, *args, **kw)
@@ -176,10 +181,13 @@
             clauses.append(
                 "ArchivePermission.packageset = %s" % sqlvalues(item.id))
             prejoins.append("packageset")
+        elif (zope_isinstance(item, DBItem) and
+              item.enum.name == "PackagePublishingPocket"):
+            clauses.append("ArchivePermission.pocket = %s" % sqlvalues(item))
         else:
             raise AssertionError(
-                "'item' %r is not an IComponent, IPackageset or an "
-                "ISourcePackageName" % item)
+                "'item' %r is not an IComponent, IPackageset, "
+                "ISourcePackageName or PackagePublishingPocket" % item)
 
         query = " AND ".join(clauses)
         auth = ArchivePermission.select(
@@ -235,7 +243,7 @@
             prejoins=["component"])
 
     def componentsForUploader(self, archive, person):
-        """See `IArchivePermissionSet`,"""
+        """See `IArchivePermissionSet`."""
         return self._componentsFor(
             archive, person, ArchivePermissionType.UPLOAD)
 
@@ -277,6 +285,24 @@
             sourcepackagename=sourcepackagename)
         return results.prejoin(["sourcepackagename"])
 
+    def pocketsForUploader(self, archive, person):
+        """See `IArchivePermissionSet`."""
+        return ArchivePermission.select("""
+            ArchivePermission.archive = %s AND
+            ArchivePermission.permission = %s AND
+            ArchivePermission.pocket IS NOT NULL AND
+            EXISTS (SELECT TeamParticipation.person
+                    FROM TeamParticipation
+                    WHERE TeamParticipation.person = %s AND
+                    TeamParticipation.team = ArchivePermission.person)
+            """ % sqlvalues(archive, ArchivePermissionType.UPLOAD, person))
+
+    def uploadersForPocket(self, archive, pocket):
+        "See `IArchivePermissionSet`."""
+        return ArchivePermission.selectBy(
+            archive=archive, permission=ArchivePermissionType.UPLOAD,
+            pocket=pocket)
+
     def queueAdminsForComponent(self, archive, component):
         "See `IArchivePermissionSet`."""
         component = self._nameToComponent(component)
@@ -315,6 +341,17 @@
                 archive=archive, person=person, component=component,
                 permission=ArchivePermissionType.UPLOAD)
 
+    def newPocketUploader(self, archive, person, pocket):
+        """See `IArchivePermissionSet`."""
+        existing = self.checkAuthenticated(
+            person, archive, ArchivePermissionType.UPLOAD, pocket)
+        try:
+            return existing[0]
+        except IndexError:
+            return ArchivePermission(
+                archive=archive, person=person, pocket=pocket,
+                permission=ArchivePermissionType.UPLOAD)
+
     def newQueueAdmin(self, archive, person, component):
         """See `IArchivePermissionSet`."""
         component = self._nameToComponent(component)
@@ -353,6 +390,12 @@
             permission=ArchivePermissionType.UPLOAD)
         self._remove_permission(permission)
 
+    def deletePocketUploader(self, archive, person, pocket):
+        permission = ArchivePermission.selectOneBy(
+            archive=archive, person=person, pocket=pocket,
+            permission=ArchivePermissionType.UPLOAD)
+        self._remove_permission(permission)
+
     def deleteQueueAdmin(self, archive, person, component):
         """See `IArchivePermissionSet`."""
         component = self._nameToComponent(component)

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2012-05-29 15:55:23 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2012-06-07 17:18:24 +0000
@@ -24,6 +24,7 @@
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.enums import BuildStatus
+from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import (
     IPersonSet,
     TeamSubscriptionPolicy,
@@ -64,10 +65,12 @@
     InvalidPocketForPPA,
     NoRightsForArchive,
     NoRightsForComponent,
+    NoSuchPPA,
     VersionRequiresName,
     )
 from lp.soyuz.interfaces.archivearch import IArchiveArchSet
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
+from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
 from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
@@ -92,6 +95,7 @@
     )
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
     )
 from lp.testing.sampledata import COMMERCIAL_ADMIN_EMAIL
@@ -523,11 +527,11 @@
         # Somebody unrelated does not
         self.assertFalse(archive.checkArchivePermission(somebody))
 
-    def makeArchiveAndActiveDistroSeries(self, purpose=ArchivePurpose.PRIMARY):
+    def makeArchiveAndActiveDistroSeries(self, purpose=ArchivePurpose.PRIMARY,
+                                         status=SeriesStatus.DEVELOPMENT):
         archive = self.factory.makeArchive(purpose=purpose)
         distroseries = self.factory.makeDistroSeries(
-            distribution=archive.distribution,
-            status=SeriesStatus.DEVELOPMENT)
+            distribution=archive.distribution, status=status)
         return archive, distroseries
 
     def makePersonWithComponentPermission(self, archive):
@@ -706,6 +710,21 @@
         self.assertCanUpload(
             archive, person, sourcepackagename, distroseries=distroseries)
 
+    def makePersonWithPocketPermission(self, archive, pocket):
+        person = self.factory.makePerson()
+        removeSecurityProxy(archive).newPocketUploader(person, pocket)
+        return person
+
+    def test_checkUpload_pocket_permission(self):
+        archive, distroseries = self.makeArchiveAndActiveDistroSeries(
+            purpose=ArchivePurpose.PRIMARY, status=SeriesStatus.CURRENT)
+        sourcepackagename = self.factory.makeSourcePackageName()
+        pocket = PackagePublishingPocket.SECURITY
+        person = self.makePersonWithPocketPermission(archive, pocket)
+        self.assertCanUpload(
+            archive, person, sourcepackagename, distroseries=distroseries,
+            pocket=pocket)
+
     def make_person_with_packageset_permission(self, archive, distroseries,
                                                packages=()):
         packageset = self.factory.makePackageset(
@@ -801,11 +820,10 @@
 
     def makePackageToUpload(self, distroseries):
         sourcepackagename = self.factory.makeSourcePackageName()
-        suitesourcepackage = self.factory.makeSuiteSourcePackage(
+        return self.factory.makeSuiteSourcePackage(
             pocket=PackagePublishingPocket.RELEASE,
             sourcepackagename=sourcepackagename,
             distroseries=distroseries)
-        return suitesourcepackage
 
     def test_canUploadSuiteSourcePackage_invalid_pocket(self):
         # Test that canUploadSuiteSourcePackage calls checkUpload for
@@ -927,6 +945,7 @@
         self.archive.updatePackageDownloadCount(
             self.bpr_1, day, self.australia, 10)
         self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
+        self.assertEqual(10, self.archive.getPackageDownloadTotal(self.bpr_1))
 
     def test_reuses_existing_entry(self):
         # A second update will simply add to the count on the existing
@@ -937,6 +956,7 @@
         self.archive.updatePackageDownloadCount(
             self.bpr_1, day, self.australia, 3)
         self.assertCount(13, self.archive, self.bpr_1, day, self.australia)
+        self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
 
     def test_differentiates_between_countries(self):
         # A different country will cause a new entry to be created.
@@ -948,6 +968,7 @@
 
         self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
         self.assertCount(3, self.archive, self.bpr_1, day, self.new_zealand)
+        self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
 
     def test_country_can_be_none(self):
         # The country can be None, indicating that it is unknown.
@@ -959,6 +980,7 @@
 
         self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
         self.assertCount(3, self.archive, self.bpr_1, day, None)
+        self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
 
     def test_differentiates_between_days(self):
         # A different date will also cause a new entry to be created.
@@ -972,6 +994,7 @@
         self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
         self.assertCount(
             3, self.archive, self.bpr_1, another_day, self.australia)
+        self.assertEqual(13, self.archive.getPackageDownloadTotal(self.bpr_1))
 
     def test_differentiates_between_bprs(self):
         # And even a different package will create a new entry.
@@ -983,6 +1006,8 @@
 
         self.assertCount(10, self.archive, self.bpr_1, day, self.australia)
         self.assertCount(3, self.archive, self.bpr_2, day, self.australia)
+        self.assertEqual(10, self.archive.getPackageDownloadTotal(self.bpr_1))
+        self.assertEqual(3, self.archive.getPackageDownloadTotal(self.bpr_2))
 
 
 class TestEnabledRestrictedBuilds(TestCaseWithFactory):
@@ -1015,11 +1040,9 @@
         distro.main_archive.require_virtualized = False
         # Restricting to all restricted architectures is fine
         distro.main_archive.enabled_restricted_families = [self.arm]
-
-        def restrict():
-            distro.main_archive.enabled_restricted_families = []
-
-        self.assertRaises(CannotRestrictArchitectures, restrict)
+        self.assertRaises(
+            CannotRestrictArchitectures, setattr, distro.main_archive,
+            "enabled_restricted_families", [])
 
     def test_main_virtualized_archive_can_be_restricted(self):
         # A main archive can be restricted to certain architectures
@@ -1071,15 +1094,63 @@
         self.assertContentEqual([], self.archive.enabled_restricted_families)
 
 
+class TestBuilddSecret(TestCaseWithFactory):
+    """Test buildd_secret security.
+
+    The buildd_secret is used by the slave scanner when generating a
+    sources.list entry for the builder to access a private archive.  It is
+    essentially the password to the archive for the builder.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestBuilddSecret, self).setUp()
+        self.archive = self.factory.makeArchive()
+
+    def test_anonymous_cannot_set_buildd_secret(self):
+        login(ANONYMOUS)
+        e = self.assertRaises(
+            Unauthorized, setattr, self.archive, "buildd_secret", "boing")
+        self.assertEqual("launchpad.Commercial", e.args[2])
+
+    def test_commercial_admin_can_set_buildd_secret(self):
+        with celebrity_logged_in("commercial_admin"):
+            self.archive.buildd_secret = "not so secret at all"
+
+    def test_admin_can_set_buildd_secret(self):
+        with celebrity_logged_in("admin"):
+            self.archive.buildd_secret = "not so secret"
+
+    def test_public_archive_has_public_buildd_secret(self):
+        # In a public PPA, the buildd "secret" is visible to anyone.
+        with celebrity_logged_in("admin"):
+            self.archive.buildd_secret = "not so secret"
+        login(ANONYMOUS)
+        self.assertFalse(self.archive.private)
+        self.assertEqual("not so secret", self.archive.buildd_secret)
+
+    def test_private_archive_has_private_buildd_secret(self):
+        # In a private PPA, the buildd secret can only be read by users with
+        # launchpad.View on the archive.
+        with celebrity_logged_in("admin"):
+            self.archive.buildd_secret = "really secret"
+            self.archive.private = True
+        login(ANONYMOUS)
+        e = self.assertRaises(
+            Unauthorized, getattr, self.archive, "buildd_secret")
+        self.assertEqual("launchpad.View", e.args[2])
+        with person_logged_in(self.archive.owner):
+            self.assertEqual("really secret", self.archive.buildd_secret)
+
+
 class TestArchiveTokens(TestCaseWithFactory):
     layer = LaunchpadZopelessLayer
 
     def setUp(self):
         super(TestArchiveTokens, self).setUp()
         owner = self.factory.makePerson()
-        self.private_ppa = self.factory.makeArchive(owner=owner)
-        self.private_ppa.buildd_secret = 'blah'
-        self.private_ppa.private = True
+        self.private_ppa = self.factory.makeArchive(owner=owner, private=True)
         self.joe = self.factory.makePerson(name='joe')
         self.private_ppa.newSubscription(self.joe, owner)
 
@@ -1628,8 +1699,7 @@
         # By default, a person cannot upload to any component of an archive.
         archive = self.factory.makeArchive()
         person = self.factory.makePerson()
-        self.assertEqual(set(),
-            set(archive.getComponentsForUploader(person)))
+        self.assertFalse(set(archive.getComponentsForUploader(person)))
 
     def test_components_for_person_with_permissions(self):
         # If a person has been explicitly granted upload permissions to a
@@ -1646,6 +1716,30 @@
             set(archive.getComponentsForUploader(person)))
 
 
+class TestPockets(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_no_pockets_for_arbitrary_person(self):
+        # By default, a person cannot upload to any pocket of an archive.
+        archive = self.factory.makeArchive()
+        person = self.factory.makePerson()
+        self.assertEqual(set(), set(archive.getPocketsForUploader(person)))
+
+    def test_pockets_for_person_with_permissions(self):
+        # If a person has been explicitly granted upload permissions to a
+        # particular pocket, then those pockets are included in
+        # IArchive.getPocketsForUploader.
+        archive = self.factory.makeArchive()
+        person = self.factory.makePerson()
+        # Only admins or techboard members can add permissions normally. That
+        # restriction isn't relevant to this test.
+        ap_set = removeSecurityProxy(getUtility(IArchivePermissionSet))
+        ap = ap_set.newPocketUploader(
+            archive, person, PackagePublishingPocket.SECURITY)
+        self.assertEqual(set([ap]), set(archive.getPocketsForUploader(person)))
+
+
 class TestValidatePPA(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
@@ -2480,3 +2574,254 @@
         with person_logged_in(archive2.owner):
             self.assertRaises(
                 AssertionError, archive2.removeCopyNotification, job2.id)
+
+
+class TestPublishFlag(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_primary_archive_published_by_default(self):
+        distribution = self.factory.makeDistribution()
+        self.assertTrue(distribution.main_archive.publish)
+
+    def test_partner_archive_published_by_default(self):
+        partner = self.factory.makeArchive(purpose=ArchivePurpose.PARTNER)
+        self.assertTrue(partner.publish)
+
+    def test_ppa_published_by_default(self):
+        ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+        self.assertTrue(ppa.publish)
+
+    def test_copy_archive_not_published_by_default(self):
+        copy = self.factory.makeArchive(purpose=ArchivePurpose.COPY)
+        self.assertFalse(copy.publish)
+
+
+class TestPPANaming(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_unique_copy_archive_name(self):
+        # Non-PPA archive names must be unique for a given distribution.
+        uber = self.factory.makeDistribution()
+        self.factory.makeArchive(
+            purpose=ArchivePurpose.COPY, distribution=uber, name="uber-copy")
+        self.assertRaises(
+            AssertionError, self.factory.makeArchive,
+            purpose=ArchivePurpose.COPY, distribution=uber, name="uber-copy")
+
+    def test_unique_partner_archive_name(self):
+        # Partner archive names must be unique for a given distribution.
+        uber = self.factory.makeDistribution()
+        self.factory.makeArchive(
+            purpose=ArchivePurpose.PARTNER, distribution=uber,
+            name="uber-partner")
+        self.assertRaises(
+            AssertionError, self.factory.makeArchive,
+            purpose=ArchivePurpose.PARTNER, distribution=uber,
+            name="uber-partner")
+
+    def test_unique_ppa_name_per_owner_and_distribution(self):
+        person = self.factory.makePerson()
+        self.factory.makeArchive(owner=person, name="ppa")
+        self.assertEqual(
+            "PPA for %s" % person.displayname, person.archive.displayname)
+        self.assertEqual("ppa", person.archive.name)
+        self.assertRaises(
+            AssertionError, self.factory.makeArchive, owner=person, name="ppa")
+
+    def test_default_archive(self):
+        # Creating multiple PPAs does not affect the existing traversal from
+        # IPerson to a single IArchive.
+        person = self.factory.makePerson()
+        ppa = self.factory.makeArchive(owner=person, name="ppa")
+        self.factory.makeArchive(owner=person, name="nightly")
+        self.assertEqual(ppa, person.archive)
+
+    def test_non_default_ppas_have_different_displayname(self):
+        person = self.factory.makePerson()
+        another_ppa = self.factory.makeArchive(owner=person, name="nightly")
+        self.assertEqual(
+            "PPA named nightly for %s" % person.displayname,
+            another_ppa.displayname)
+
+    def test_archives_cannot_have_same_name_as_distribution(self):
+        boingolinux = self.factory.makeDistribution(name="boingolinux")
+        self.assertRaises(
+            AssertionError, getUtility(IArchiveSet).new,
+            owner=self.factory.makePerson(), purpose=ArchivePurpose.PRIMARY,
+            distribution=boingolinux, name=boingolinux.name)
+
+
+class TestPPALookup(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPPALookup, self).setUp()
+        self.person = self.factory.makePerson()
+        self.factory.makeArchive(owner=self.person, name="ppa")
+        self.nightly = self.factory.makeArchive(
+            owner=self.person, name="nightly")
+
+    def test_ppas(self):
+        # IPerson.ppas returns all owned PPAs ordered by name.
+        self.assertEqual(
+            ["nightly", "ppa"], [ppa.name for ppa in self.person.ppas])
+
+    def test_getPPAByName(self):
+        default_ppa = self.person.getPPAByName("ppa")
+        self.assertEqual(self.person.archive, default_ppa)
+        nightly_ppa = self.person.getPPAByName("nightly")
+        self.assertEqual(self.nightly, nightly_ppa)
+
+    def test_NoSuchPPA(self):
+        self.assertRaises(NoSuchPPA, self.person.getPPAByName, "not-found")
+
+
+class TestDisplayName(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_default(self):
+        # If 'displayname' is omitted when creating the archive, there is a
+        # sensible default.
+        archive = self.factory.makeArchive(name="test-ppa")
+        self.assertEqual(
+            "PPA named test-ppa for %s" % archive.owner.displayname,
+            archive.displayname)
+
+    def test_provided(self):
+        # If 'displayname' is provided, it is used.
+        archive = self.factory.makeArchive(
+            purpose=ArchivePurpose.COPY,
+            displayname="Rock and roll with rebuilds!", name="test-rebuild")
+        self.assertEqual("Rock and roll with rebuilds!", archive.displayname)
+
+    def test_editable(self):
+        # Anyone with edit permission on the archive can change displayname.
+        archive = self.factory.makeArchive(name="test-ppa")
+        login("no-priv@xxxxxxxxxxxxx")
+        e = self.assertRaises(
+            Unauthorized, setattr, archive, "displayname", "No-way!")
+        self.assertEqual("launchpad.Edit", e.args[2])
+        with person_logged_in(archive.owner):
+            archive.displayname = "My testing packages"
+
+
+class TestSigningKeyPropagation(TestCaseWithFactory):
+    """Signing keys are shared between PPAs owned by the same person/team."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_ppa_created_with_no_signing_key(self):
+        ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+        self.assertIsNone(ppa.signing_key)
+
+    def test_default_signing_key_propagated_to_new_ppa(self):
+        person = self.factory.makePerson()
+        ppa = self.factory.makeArchive(
+            owner=person, purpose=ArchivePurpose.PPA, name="ppa")
+        self.assertEqual(ppa, person.archive)
+        self.factory.makeGPGKey(person)
+        with celebrity_logged_in("admin"):
+            person.archive.signing_key = person.gpg_keys[0]
+        ppa_with_key = self.factory.makeArchive(
+            owner=person, purpose=ArchivePurpose.PPA)
+        self.assertEqual(person.gpg_keys[0], ppa_with_key.signing_key)
+
+
+class TestCountersAndSummaries(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def assertDictEqual(self, one, two):
+        self.assertContentEqual(one.items(), two.items())
+
+    def test_cprov_build_counters_in_sampledata(self):
+        cprov_archive = getUtility(IPersonSet).getByName("cprov").archive
+        expected_counters = {
+            "failed": 1,
+            "pending": 0,
+            "succeeded": 3,
+            "superseded": 0,
+            "total": 4,
+            }
+        self.assertDictEqual(
+            expected_counters, cprov_archive.getBuildCounters())
+
+    def test_ubuntu_build_counters_in_sampledata(self):
+        ubuntu_archive = getUtility(IDistributionSet)["ubuntu"].main_archive
+        expected_counters = {
+            "failed": 5,
+            "pending": 2,
+            "succeeded": 8,
+            "superseded": 3,
+            "total": 18,
+            }
+        self.assertDictEqual(
+            expected_counters, ubuntu_archive.getBuildCounters())
+        # include_needsbuild=False excludes builds in status NEEDSBUILD.
+        expected_counters["pending"] -= 1
+        expected_counters["total"] -= 1
+        self.assertDictEqual(
+            expected_counters,
+            ubuntu_archive.getBuildCounters(include_needsbuild=False))
+
+    def assertBuildSummaryMatches(self, status, builds, summary):
+        self.assertEqual(status, summary["status"])
+        self.assertContentEqual(
+            builds, [build.title for build in summary["builds"]])
+
+    def test_build_summaries_in_sampledata(self):
+        ubuntu = getUtility(IDistributionSet)["ubuntu"]
+        firefox_source = ubuntu.getSourcePackage("mozilla-firefox")
+        firefox_source_pub = firefox_source.publishing_history[0]
+        foobar = ubuntu.getSourcePackage("foobar")
+        foobar_pub = foobar.publishing_history[0]
+        build_summaries = ubuntu.main_archive.getBuildSummariesForSourceIds(
+            [firefox_source_pub.id, foobar_pub.id])
+        self.assertEqual(2, len(build_summaries))
+        expected_firefox_builds = [
+            "hppa build of mozilla-firefox 0.9 in ubuntu warty RELEASE",
+            "i386 build of mozilla-firefox 0.9 in ubuntu warty RELEASE",
+            ]
+        self.assertBuildSummaryMatches(
+            BuildSetStatus.FULLYBUILT, expected_firefox_builds,
+            build_summaries[firefox_source_pub.id])
+        expected_foobar_builds = [
+            "i386 build of foobar 1.0 in ubuntu warty RELEASE",
+            ]
+        self.assertBuildSummaryMatches(
+            BuildSetStatus.FAILEDTOBUILD, expected_foobar_builds,
+            build_summaries[foobar_pub.id])
+
+    def test_private_archives_have_private_counters_and_summaries(self):
+        archive = self.factory.makeArchive()
+        distroseries = self.factory.makeDistroSeries(
+            distribution=archive.distribution)
+        with celebrity_logged_in("admin"):
+            archive.private = True
+            publisher = SoyuzTestPublisher()
+            publisher.setUpDefaultDistroSeries(distroseries)
+            publisher.addFakeChroots(distroseries)
+            publisher.getPubBinaries(archive=archive)
+            source_id = archive.getPublishedSources()[0].id
+
+            # An admin can see the counters and build summaries.
+            archive.getBuildCounters()["total"]
+            archive.getBuildSummariesForSourceIds([source_id])
+
+        # The archive owner can see the counters and build summaries.
+        with person_logged_in(archive.owner):
+            archive.getBuildCounters()["total"]
+            archive.getBuildSummariesForSourceIds([source_id])
+
+        # The public cannot.
+        login("no-priv@xxxxxxxxxxxxx")
+        e = self.assertRaises(
+            Unauthorized, getattr, archive, "getBuildCounters")
+        self.assertEqual("launchpad.View", e.args[2])
+        e = self.assertRaises(
+            Unauthorized, getattr, archive, "getBuildSummariesForSourceIds")
+        self.assertEqual("launchpad.View", e.args[2])

=== modified file 'lib/lp/soyuz/tests/test_archivearch.py'
--- lib/lp/soyuz/tests/test_archivearch.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/tests/test_archivearch.py	2012-06-07 17:18:24 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test ArchiveArch features."""
@@ -70,8 +70,30 @@
         # Test ArchiveArchSet.getByArchive returns no other archives.
         self.archive_arch_set.new(self.ppa, self.cell_proc)
         self.archive_arch_set.new(self.ubuntu_archive, self.omap)
+        result_set = list(self.archive_arch_set.getByArchive(self.ppa))
+        self.assertEquals(1, len(result_set))
+        self.assertEquals(self.ppa, result_set[0].archive)
+        self.assertEquals(self.cell_proc, result_set[0].processorfamily)
+
+    def test_getByArchive_follows_creation_order(self):
+        # The result of ArchiveArchSet.getByArchive follows the order in
+        # which architecture associations were added.
+        self.archive_arch_set.new(self.ppa, self.cell_proc)
+        self.archive_arch_set.new(self.ppa, self.omap)
+        result_set = list(self.archive_arch_set.getByArchive(self.ppa))
+        self.assertEqual(2, len(result_set))
+        self.assertEquals(self.ppa, result_set[0].archive)
+        self.assertEqual(self.cell_proc, result_set[0].processorfamily)
+        self.assertEquals(self.ppa, result_set[1].archive)
+        self.assertEqual(self.omap, result_set[1].processorfamily)
+
+    def test_getByArchive_specific_architecture(self):
+        # ArchiveArchSet.getByArchive can query for a specific architecture
+        # association.
+        self.archive_arch_set.new(self.ppa, self.cell_proc)
+        self.archive_arch_set.new(self.ppa, self.omap)
         result_set = list(
-            self.archive_arch_set.getByArchive(self.ppa))
+            self.archive_arch_set.getByArchive(self.ppa, self.cell_proc))
         self.assertEquals(1, len(result_set))
         self.assertEquals(self.ppa, result_set[0].archive)
         self.assertEquals(self.cell_proc, result_set[0].processorfamily)

=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py	2012-05-30 08:50:50 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py	2012-06-07 17:18:24 +0000
@@ -115,9 +115,7 @@
         self.test_publisher.prepareBreezyAutotest()
         ppa = self.factory.makeArchive(
             distribution=self.test_publisher.ubuntutest,
-            purpose=ArchivePurpose.PPA)
-        ppa.buildd_secret = 'x'
-        ppa.private = True
+            purpose=ArchivePurpose.PPA, private=True)
 
         changesfile_path = (
             'lib/lp/archiveuploader/tests/data/suite/'


Follow ups