← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/export-change-override into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/export-change-override into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #853831 in Launchpad itself: "Export SPPH.changeOverride and BPPH.changeOverride"
  https://bugs.launchpad.net/launchpad/+bug/853831

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/export-change-override/+merge/109549

== Summary ==

change-override.py, used frequently by Ubuntu archive administrators, requires direct database access (bug 853831).

== Proposed fix ==

Export SPPH.changeOverride and BPPH.changeOverride.  It will then be easy to write an API client replacing change-override.py.

== Implementation details ==

The exports require a slight rearrangement of the *PPH interfaces, creating IBinaryPackagePublishingHistoryEdit and ISourcePackagePublishingHistoryEdit which inherit from the existing IPublishingEdit base class.  It isn't possible to add changeOverride to IPublishingEdit directly because it has different parameters in the source and binary cases.

I moved ArchiveOverriderError from lp.soyuz.scripts.changeoverride to lp.soyuz.model.publishing, to stop the latter relying on the former.

In order to make this testable within the existing publishing webservice tests, I had to fix a buglet whereby changeOverride complains about a component override requiring a new archive even if it wasn't actually asked to change the component.  This doesn't normally make any difference, but the webservice tests work in a PPA and in that case it does.

I did seriously contemplate refactoring the doctests here as unit tests and avoiding sampledata and the like, which would have made it much more straightforward to do some tests using a PRIMARY archive instead, and in fact I did start in on that; but I eventually decided it would make the branch too unwieldy and unclear.  I've kept my progress on that around and will probably submit it as a separate branch at some point.

I took care to export these new methods only on devel.

This may not be desperately performant when overriding large numbers of binary packages.  I'm not too worried about this as an operational showstopper, and I don't believe that this branch will slow any of the current interfaces down, but I'd take suggestions for alternative ways to approach the new interfaces; something on Archive maybe?  (See also https://code.launchpad.net/~cjwatson/launchpad/queue-api/+merge/108967.)

== LOC Rationale ==

+134, but there's at minimum 210 lines of scripts/ftpmaster-tools/change-override.py and lib/lp/soyuz/scripts/changeoverride.py that I will remove once this has been deployed and I've written an API client, not to mention probably the best part of the 503 lines of lib/lp/soyuz/scripts/tests/test_changeoverride.py.

== Tests ==

bin/test -vvct xx-source-package-publishing.txt -t xx-binary-package-publishing.txt

== Demo and Q/A ==

Using lp-shell on qastaging, use *PPH.changeOverride on some randomly selected source and binary publication records (perhaps based on http://people.canonical.com/~ubuntu-archive/component-mismatches.txt), and verify that this causes the corresponding +publishinghistory entries to change.

== Lint ==

Pre-existing lint, not worth fixing here:

./lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
     192: want exceeds 78 characters.
     210: want exceeds 78 characters.
     220: source exceeds 78 characters.
     225: source exceeds 78 characters.
     230: source exceeds 78 characters.
     235: source exceeds 78 characters.
     240: source exceeds 78 characters.
-- 
https://code.launchpad.net/~cjwatson/launchpad/export-change-override/+merge/109549
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/export-change-override 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-11 01:24:27 +0000
@@ -199,7 +199,9 @@
 from lp.soyuz.interfaces.processor import IProcessorFamily
 from lp.soyuz.interfaces.publishing import (
     IBinaryPackagePublishingHistory,
+    IBinaryPackagePublishingHistoryEdit,
     ISourcePackagePublishingHistory,
+    ISourcePackagePublishingHistoryEdit,
     ISourcePackagePublishingHistoryPublic,
     )
 from lp.soyuz.interfaces.queue import IPackageUpload
@@ -372,6 +374,12 @@
     ISourcePackagePublishingHistory)
 patch_reference_property(
     ISourcePackagePublishingHistory, 'packageupload', IPackageUpload)
+patch_entry_return_type(
+    ISourcePackagePublishingHistoryEdit, 'changeOverride',
+    ISourcePackagePublishingHistory)
+patch_entry_return_type(
+    IBinaryPackagePublishingHistoryEdit, 'changeOverride',
+    IBinaryPackagePublishingHistory)
 
 # IArchive apocalypse.
 patch_reference_property(IArchive, 'distribution', IDistribution)

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2012-06-09 16:26:32 +0000
+++ lib/lp/soyuz/configure.zcml	2012-06-11 01:24:27 +0000
@@ -38,7 +38,7 @@
                 interface="lp.soyuz.interfaces.publishing.IBinaryPackagePublishingHistoryPublic"/>
             <require
                 permission="launchpad.Edit"
-                interface="lp.soyuz.interfaces.publishing.IPublishingEdit"/>
+                interface="lp.soyuz.interfaces.publishing.IBinaryPackagePublishingHistoryEdit"/>
             <require
                 permission="launchpad.Admin"
                 set_schema="lp.soyuz.interfaces.publishing.IBinaryPackagePublishingHistory"/>
@@ -108,7 +108,7 @@
                 interface="lp.soyuz.interfaces.publishing.ISourcePackagePublishingHistoryPublic"/>
             <require
                 permission="launchpad.Edit"
-                interface="lp.soyuz.interfaces.publishing.IPublishingEdit"/>
+                interface="lp.soyuz.interfaces.publishing.ISourcePackagePublishingHistoryEdit"/>
             <require
                 permission="launchpad.Admin"
                 set_schema="lp.soyuz.interfaces.publishing.ISourcePackagePublishingHistory"/>

=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
--- lib/lp/soyuz/interfaces/publishing.py	2012-05-21 07:34:15 +0000
+++ lib/lp/soyuz/interfaces/publishing.py	2012-06-11 01:24:27 +0000
@@ -11,6 +11,7 @@
     'IArchiveSafePublisher',
     'IBinaryPackageFilePublishing',
     'IBinaryPackagePublishingHistory',
+    'IBinaryPackagePublishingHistoryEdit',
     'IBinaryPackagePublishingHistoryPublic',
     'ICanPublishPackages',
     'IFilePublishing',
@@ -18,17 +19,22 @@
     'IPublishingSet',
     'ISourcePackageFilePublishing',
     'ISourcePackagePublishingHistory',
+    'ISourcePackagePublishingHistoryEdit',
     'ISourcePackagePublishingHistoryPublic',
     'MissingSymlinkInPool',
     'NotInPool',
+    'OverrideError',
     'PoolFileOverwriteError',
     'active_publishing_status',
     'inactive_publishing_status',
     'name_priority_map',
     ]
 
+import httplib
+
 from lazr.restful.declarations import (
     call_with,
+    error_status,
     export_as_webservice_entry,
     export_operation_as,
     export_read_operation,
@@ -37,6 +43,7 @@
     operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
+    operation_returns_entry,
     REQUEST_USER,
     )
 from lazr.restful.fields import Reference
@@ -65,6 +72,7 @@
 from lp.soyuz.interfaces.binarypackagerelease import (
     IBinaryPackageReleaseDownloadCount,
     )
+from lp.soyuz.scripts.ftpmasterbase import SoyuzScriptError
 
 #
 # Exceptions
@@ -95,6 +103,14 @@
     continues.
     """
 
+
+@error_status(httplib.BAD_REQUEST)
+# XXX cjwatson 2012-06-07: SoyuzScriptError should be changed to Exception
+# once lp.soyuz.scripts.changeoverride is removed.
+class OverrideError(SoyuzScriptError):
+    """Raised when an attempt to change an override fails."""
+
+
 name_priority_map = {
     'required': PackagePublishingPriority.REQUIRED,
     'important': PackagePublishingPriority.IMPORTANT,
@@ -626,16 +642,6 @@
             logged.
         """
 
-    def changeOverride(new_component=None, new_section=None):
-        """Change the component and/or section of this publication
-
-        It is changed only if the argument is not None.
-
-        Return the overridden publishing record, either a
-        `ISourcePackagePublishingHistory` or
-        `IBinaryPackagePublishingHistory`.
-        """
-
     def copyTo(distroseries, pocket, archive, overrides=None, creator=None):
         """Copy this publication to another location.
 
@@ -695,8 +701,29 @@
         """
 
 
+class ISourcePackagePublishingHistoryEdit(IPublishingEdit):
+    """A writeable source package publishing history record."""
+
+    # Really ISourcePackagePublishingHistory, patched in
+    # _schema_circular_imports.py.
+    @operation_returns_entry(Interface)
+    @operation_parameters(
+        new_component=TextLine(title=u"The new component name."),
+        new_section=TextLine(title=u"The new section name."))
+    @export_write_operation()
+    @operation_for_version("devel")
+    def changeOverride(new_component=None, new_section=None):
+        """Change the component and/or section of this publication.
+
+        It is changed only if the argument is not None.
+
+        Return the overridden publishing record, a
+        `ISourcePackagePublishingHistory`.
+        """
+
+
 class ISourcePackagePublishingHistory(ISourcePackagePublishingHistoryPublic,
-                                      IPublishingEdit):
+                                      ISourcePackagePublishingHistoryEdit):
     """A source package publishing history record."""
     export_as_webservice_entry(publish_web_link=False)
 
@@ -875,17 +902,6 @@
             logged.
         """
 
-    def changeOverride(new_component=None, new_section=None,
-                       new_priority=None):
-        """Change the component, section and/or priority of this publication.
-
-        It is changed only if the argument is not None.
-
-        Return the overridden publishing record, either a
-        `ISourcePackagePublishingHistory` or
-        `IBinaryPackagePublishingHistory`.
-        """
-
     def copyTo(distroseries, pocket, archive):
         """Copy this publication to another location.
 
@@ -934,8 +950,31 @@
         """
 
 
+class IBinaryPackagePublishingHistoryEdit(IPublishingEdit):
+    """A writeable binary package publishing record."""
+
+    # Really IBinaryPackagePublishingHistory, patched in
+    # _schema_circular_imports.py.
+    @operation_returns_entry(Interface)
+    @operation_parameters(
+        new_component=TextLine(title=u"The new component name."),
+        new_section=TextLine(title=u"The new section name."),
+        new_priority=TextLine(title=u"The new priority name."))
+    @export_write_operation()
+    @operation_for_version("devel")
+    def changeOverride(new_component=None, new_section=None,
+                       new_priority=None):
+        """Change the component, section and/or priority of this publication.
+
+        It is changed only if the argument is not None.
+
+        Return the overridden publishing record, a
+        `IBinaryPackagePublishingHistory`.
+        """
+
+
 class IBinaryPackagePublishingHistory(IBinaryPackagePublishingHistoryPublic,
-                                      IPublishingEdit):
+                                      IBinaryPackagePublishingHistoryEdit):
     """A binary package publishing record."""
     export_as_webservice_entry(publish_web_link=False)
 

=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py	2012-05-21 07:34:15 +0000
+++ lib/lp/soyuz/model/publishing.py	2012-06-11 01:24:27 +0000
@@ -84,6 +84,7 @@
     BuildSetStatus,
     IBinaryPackageBuildSet,
     )
+from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.distributionjob import (
     IDistroSeriesDifferenceJobSource,
     )
@@ -94,9 +95,12 @@
     IPublishingSet,
     ISourcePackageFilePublishing,
     ISourcePackagePublishingHistory,
+    name_priority_map,
+    OverrideError,
     PoolFileOverwriteError,
     )
 from lp.soyuz.interfaces.queue import QueueInconsistentStateError
+from lp.soyuz.interfaces.section import ISectionSet
 from lp.soyuz.model.binarypackagename import BinaryPackageName
 from lp.soyuz.model.binarypackagerelease import (
     BinaryPackageRelease,
@@ -108,7 +112,6 @@
     )
 from lp.soyuz.model.packagediff import PackageDiff
 from lp.soyuz.pas import determineArchitecturesToBuild
-from lp.soyuz.scripts.changeoverride import ArchiveOverriderError
 
 
 def makePoolPath(source_name, component_name):
@@ -793,22 +796,27 @@
         # Check there is a change to make
         if new_component is None:
             new_component = current.component
+        elif isinstance(new_component, basestring):
+            new_component = getUtility(IComponentSet)[new_component]
         if new_section is None:
             new_section = current.section
+        elif isinstance(new_section, basestring):
+            new_section = getUtility(ISectionSet)[new_section]
 
         if (new_component == current.component and
             new_section == current.section):
             return
 
-        # See if the archive has changed by virtue of the component
-        # changing:
-        distribution = self.distroseries.distribution
-        new_archive = distribution.getArchiveByComponent(
-            new_component.name)
-        if new_archive != None and new_archive != current.archive:
-            raise ArchiveOverriderError(
-                "Overriding component to '%s' failed because it would "
-                "require a new archive." % new_component.name)
+        if new_component != current.component:
+            # See if the archive has changed by virtue of the component
+            # changing:
+            distribution = self.distroseries.distribution
+            new_archive = distribution.getArchiveByComponent(
+                new_component.name)
+            if new_archive != None and new_archive != current.archive:
+                raise OverrideError(
+                    "Overriding component to '%s' failed because it would "
+                    "require a new archive." % new_component.name)
 
         return getUtility(IPublishingSet).newSourcePublication(
             distroseries=current.distroseries,
@@ -1208,24 +1216,32 @@
         # Check there is a change to make
         if new_component is None:
             new_component = current.component
+        elif isinstance(new_component, basestring):
+            new_component = getUtility(IComponentSet)[new_component]
         if new_section is None:
             new_section = current.section
+        elif isinstance(new_section, basestring):
+            new_section = getUtility(ISectionSet)[new_section]
         if new_priority is None:
             new_priority = current.priority
+        elif isinstance(new_priority, basestring):
+            new_priority = name_priority_map[new_priority]
 
         if (new_component == current.component and
             new_section == current.section and
             new_priority == current.priority):
             return
 
-        # See if the archive has changed by virtue of the component changing:
-        distribution = self.distroarchseries.distroseries.distribution
-        new_archive = distribution.getArchiveByComponent(
-            new_component.name)
-        if new_archive != None and new_archive != self.archive:
-            raise ArchiveOverriderError(
-                "Overriding component to '%s' failed because it would "
-                "require a new archive." % new_component.name)
+        if new_component != current.component:
+            # See if the archive has changed by virtue of the component
+            # changing:
+            distribution = self.distroarchseries.distroseries.distribution
+            new_archive = distribution.getArchiveByComponent(
+                new_component.name)
+            if new_archive != None and new_archive != self.archive:
+                raise OverrideError(
+                    "Overriding component to '%s' failed because it would "
+                    "require a new archive." % new_component.name)
 
         # Append the modified package publishing entry
         return BinaryPackagePublishingHistory(

=== modified file 'lib/lp/soyuz/scripts/changeoverride.py'
--- lib/lp/soyuz/scripts/changeoverride.py	2012-04-16 23:02:44 +0000
+++ lib/lp/soyuz/scripts/changeoverride.py	2012-06-11 01:24:27 +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).
 
 """Soyuz publication override script."""
@@ -6,7 +6,6 @@
 __metaclass__ = type
 
 __all__ = [
-    'ArchiveOverriderError',
     'ChangeOverride',
     ]
 
@@ -22,13 +21,6 @@
     )
 
 
-class ArchiveOverriderError(SoyuzScriptError):
-    """ArchiveOverrider specific exception.
-
-    Mostly used to describe errors in the initialization of this object.
-    """
-
-
 class ChangeOverride(SoyuzScript):
 
     usage = '%prog -s <suite> <package name> [-SBt] [-c component]'
@@ -192,4 +184,3 @@
 
         for binary_name in sorted(binary_names):
             self.processBinaryChange(binary_name)
-

=== modified file 'lib/lp/soyuz/scripts/tests/test_changeoverride.py'
--- lib/lp/soyuz/scripts/tests/test_changeoverride.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/scripts/tests/test_changeoverride.py	2012-06-11 01:24:27 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 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).
 
 """`ChangeOverride` script class tests."""
@@ -16,11 +16,9 @@
 from lp.services.log.logger import BufferLogger
 from lp.soyuz.enums import PackagePublishingPriority
 from lp.soyuz.interfaces.component import IComponentSet
+from lp.soyuz.interfaces.publishing import OverrideError
 from lp.soyuz.interfaces.section import ISectionSet
-from lp.soyuz.scripts.changeoverride import (
-    ArchiveOverriderError,
-    ChangeOverride,
-    )
+from lp.soyuz.scripts.changeoverride import ChangeOverride
 from lp.soyuz.scripts.ftpmasterbase import SoyuzScriptError
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing.layers import LaunchpadZopelessLayer
@@ -486,11 +484,11 @@
             component="partner", section="base", priority="extra")
 
         self.assertRaises(
-            ArchiveOverriderError, changer.processSourceChange, 'boingo')
-        self.assertRaises(
-            ArchiveOverriderError, changer.processBinaryChange, 'boingo-bin')
-        self.assertRaises(
-            ArchiveOverriderError, changer.processChildrenChange, 'boingo')
+            OverrideError, changer.processSourceChange, 'boingo')
+        self.assertRaises(
+            OverrideError, changer.processBinaryChange, 'boingo-bin')
+        self.assertRaises(
+            OverrideError, changer.processChildrenChange, 'boingo')
 
     def test_target_publication_not_found(self):
         """Raises SoyuzScriptError when a source was not found."""

=== modified file 'lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt	2011-12-08 19:37:55 +0000
+++ lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt	2012-06-11 01:24:27 +0000
@@ -97,10 +97,8 @@
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...      SoyuzTestPublisher)
-    >>> from lp.soyuz.enums import (
-    ...     PackagePublishingStatus)
+    >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
+    >>> from lp.soyuz.enums import PackagePublishingStatus
     >>> cprov_db = getUtility(IPersonSet).getByName('cprov')
     >>> ubuntu_db = getUtility(IDistributionSet).getByName('ubuntu')
     >>> cprov_private_ppa_db = factory.makeArchive(
@@ -249,3 +247,57 @@
     ...     firefox['self_link'], 'getDailyDownloadTotals').jsonBody()
     {u'2010-02-21': 10, u'2010-02-23': 8}
 
+
+Overrides
+=========
+
+    >>> from lp.services.webapp.interfaces import OAuthPermission
+    >>> from lp.testing.pages import webservice_for_person
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> override_source = test_publisher.getPubSource(
+    ...     archive=cprov_db.archive, sourcename="testoverrides")
+    >>> override_binaries = test_publisher.getPubBinaries(
+    ...     binaryname="testoverrides", pub_source=override_source,
+    ...     status=PackagePublishingStatus.PUBLISHED)
+    >>> logout()
+    >>> cprov_webservice = webservice_for_person(
+    ...     cprov_db, permission=OAuthPermission.WRITE_PUBLIC)
+
+    >>> cprov_archive_devel = webservice.get(
+    ...     "/~cprov/+archive/ppa", api_version="devel").jsonBody()
+    >>> pubs = webservice.named_get(
+    ...     cprov_archive_devel["self_link"], "getPublishedBinaries",
+    ...     api_version="devel", binary_name="testoverrides").jsonBody()
+    >>> print pubs["entries"][0]["section_name"]
+    base
+    >>> print pubs["entries"][0]["priority_name"]
+    STANDARD
+    >>> package = pubs["entries"][0]["self_link"]
+
+Anonymous users can't change overrides.
+
+    >>> response = webservice.named_post(
+    ...     package, "changeOverride", api_version="devel",
+    ...     new_section="admin", new_priority="optional")
+    >>> print response
+    HTTP/1.1 401 Unauthorized
+    ...
+
+The owner of a PPA can change overrides.
+
+    >>> response = cprov_webservice.named_post(
+    ...     package, "changeOverride", api_version="devel",
+    ...     new_section="admin", new_priority="optional")
+    >>> print response
+    HTTP/1.1 200 Ok
+    ...
+
+The override change takes effect:
+
+    >>> pubs = webservice.named_get(
+    ...     cprov_archive["self_link"], "getPublishedBinaries",
+    ...     binary_name="testoverrides").jsonBody()
+    >>> print pubs["entries"][0]["section_name"]
+    admin
+    >>> print pubs["entries"][0]["priority_name"]
+    OPTIONAL

=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt	2012-05-24 22:33:41 +0000
+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt	2012-06-11 01:24:27 +0000
@@ -15,13 +15,18 @@
     >>> from zope.security.proxy import removeSecurityProxy
     >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.registry.model.gpgkey import GPGKey
+    >>> from lp.services.webapp.interfaces import OAuthPermission
+    >>> from lp.testing.pages import webservice_for_person
     >>> name16 = getUtility(IPersonSet).getByName('name16')
     >>> fake_signer = GPGKey.selectOneBy(owner=name16)
-    >>> ppa = getUtility(IPersonSet).getByName('cprov').archive
-    >>> for pub in ppa.getPublishedSources():
+    >>> cprov_db = getUtility(IPersonSet).getByName('cprov')
+    >>> cprov_ppa = cprov_db.archive
+    >>> for pub in cprov_ppa.getPublishedSources():
     ...     pub = removeSecurityProxy(pub)
     ...     pub.sourcepackagerelease.dscsigningkey = fake_signer
     >>> logout()
+    >>> cprov_webservice = webservice_for_person(
+    ...     cprov_db, permission=OAuthPermission.WRITE_PUBLIC)
 
     >>> cprov_archive = webservice.get("/~cprov/+archive/ppa").jsonBody()
     >>> cprov_srcs_response = pubs = webservice.named_get(
@@ -90,15 +95,11 @@
 publication to play with first.
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
+    >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
     >>> stp = SoyuzTestPublisher()
     >>> stp.prepareBreezyAutotest()
-    >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.distribution import IDistributionSet
-    >>> from lp.registry.interfaces.person import IPersonSet
     >>> from lp.soyuz.enums import PackagePublishingStatus
-    >>> cprov_ppa = getUtility(IPersonSet).getByName("cprov").archive
     >>> source = stp.getPubSource(
     ...     archive=cprov_ppa, sourcename="testwebservice")
     >>> binaries = stp.getPubBinaries(
@@ -156,7 +157,7 @@
 Make cprov's PPA packages unsigned:
 
     >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> for pub in ppa.getPublishedSources():
+    >>> for pub in cprov_ppa.getPublishedSources():
     ...     pub = removeSecurityProxy(pub)
     ...     pub.sourcepackagerelease.dscsigningkey = None
     >>> logout()
@@ -176,8 +177,6 @@
 
 A user can request a package to be deleted:
 
-    >>> cprov = webservice.get("/~cprov").jsonBody()
-    >>> cprov_link = cprov['self_link']
     >>> pubs = webservice.named_get(
     ...     cprov_archive['self_link'], 'getPublishedSources',
     ...     source_name="testwebservice", version="666",
@@ -197,13 +196,6 @@
 
 The owner of a PPA can delete packages.
 
-    >>> login("foo.bar@xxxxxxxxxxxxx")
-    >>> cprov = getUtility(IPersonSet).getByName('cprov')
-    >>> logout()
-    >>> from lp.testing.pages import webservice_for_person
-    >>> from lp.services.webapp.interfaces import OAuthPermission
-    >>> cprov_webservice = webservice_for_person(cprov,
-    ...     permission=OAuthPermission.WRITE_PUBLIC)
     >>> response = cprov_webservice.named_post(
     ...     package, 'requestDeletion',
     ...     removal_comment="No longer needed")
@@ -238,11 +230,6 @@
 
 Create a private PPA for Celso with some binaries.
 
-    >>> from zope.component import getUtility
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...      SoyuzTestPublisher)
-    >>> cprov_db = getUtility(IPersonSet).getByName('cprov')
     >>> ubuntu_db = getUtility(IDistributionSet).getByName('ubuntu')
     >>> cprov_private_ppa_db = factory.makeArchive(
     ...     private=True, owner=cprov_db, name="p3a",
@@ -427,9 +414,9 @@
 
     >>> login("admin@xxxxxxxxxxxxx")
     >>> to_pub = test_publisher.getPubSource(
-    ...     sourcename='difftest', version='1.0', archive=cprov.archive)
+    ...     sourcename='difftest', version='1.0', archive=cprov_db.archive)
     >>> from_pub = test_publisher.getPubSource(
-    ...     sourcename='difftest', version='1.1', archive=cprov.archive)
+    ...     sourcename='difftest', version='1.1', archive=cprov_db.archive)
     >>> new_diff = factory.makePackageDiff(
     ...     from_source=from_pub.sourcepackagerelease,
     ...     to_source=to_pub.sourcepackagerelease,
@@ -460,10 +447,53 @@
 It will match the fake content we added earlier:
 
     >>> login("admin@xxxxxxxxxxxxx")
-    >>> from lp.services.librarian.browser import (
-    ...     ProxiedLibraryFileAlias)
+    >>> from lp.services.librarian.browser import ProxiedLibraryFileAlias
     >>> diff_url == ProxiedLibraryFileAlias(
-    ...     new_diff.diff_content, cprov.archive).http_url
+    ...     new_diff.diff_content, cprov_db.archive).http_url
     True
 
     >>> logout()
+
+
+Overrides
+=========
+
+    >>> login("foo.bar@xxxxxxxxxxxxx")
+    >>> override_source = stp.getPubSource(
+    ...     archive=cprov_ppa, sourcename="testoverrides")
+    >>> logout()
+
+    >>> cprov_archive_devel = webservice.get(
+    ...     "/~cprov/+archive/ppa", api_version="devel").jsonBody()
+    >>> pubs = webservice.named_get(
+    ...     cprov_archive_devel["self_link"], "getPublishedSources",
+    ...     api_version="devel", source_name="testoverrides").jsonBody()
+    >>> print pubs["entries"][0]["section_name"]
+    base
+    >>> package = pubs["entries"][0]["self_link"]
+
+Anonymous users can't change overrides.
+
+    >>> response = webservice.named_post(
+    ...     package, "changeOverride", api_version="devel",
+    ...     new_section="admin")
+    >>> print response
+    HTTP/1.1 401 Unauthorized
+    ...
+
+The owner of a PPA can change overrides.
+
+    >>> response = cprov_webservice.named_post(
+    ...     package, "changeOverride", api_version="devel",
+    ...     new_section="admin")
+    >>> print response
+    HTTP/1.1 200 Ok
+    ...
+
+The override change takes effect:
+
+    >>> pubs = webservice.named_get(
+    ...     cprov_archive["self_link"], "getPublishedSources",
+    ...     source_name="testoverrides").jsonBody()
+    >>> print pubs["entries"][0]["section_name"]
+    admin


Follow ups