← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/ppa-api into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/ppa-api into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #776444 in Launchpad itself: "Add external dependencies for PPA via API"
  https://bugs.launchpad.net/launchpad/+bug/776444
  Bug #776449 in Launchpad itself: "Set Ubuntu dependencies for PPA via API"
  https://bugs.launchpad.net/launchpad/+bug/776449

For more details, see:
https://code.launchpad.net/~abentley/launchpad/ppa-api/+merge/63170

Summary
=======
Fix bug #776444 and #776449 about missing APIs

Proposed change
===============
Export APIs

Implementation Details
======================
Implemented _addArchiveDependency as a wrapper, because we do not want to export IComponent.

Also fixed the permissions of addArchiveDependency and removeArchiveDependency.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/lp/soyuz/browser/archive.py
  lib/lp/soyuz/browser/tests/test_archive_webservice.py
  lib/lp/soyuz/interfaces/archive.py
  lib/lp/soyuz/model/archive.py
  lib/lp/testing/factory.py

-- 
https://code.launchpad.net/~abentley/launchpad/ppa-api/+merge/63170
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/ppa-api into lp:launchpad.
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-05-17 14:27:34 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py	2011-06-02 14:48:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Update the interface schema values due to circular imports.
@@ -226,9 +226,9 @@
 IBranch['landing_candidates'].value_type.schema = IBranchMergeProposal
 IBranch['landing_targets'].value_type.schema = IBranchMergeProposal
 IBranch['linkBug'].queryTaggedValue(
-    LAZR_WEBSERVICE_EXPORTED)['params']['bug'].schema= IBug
+    LAZR_WEBSERVICE_EXPORTED)['params']['bug'].schema = IBug
 IBranch['linkSpecification'].queryTaggedValue(
-    LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema= ISpecification
+    LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema = ISpecification
 IBranch['product'].schema = IProduct
 
 patch_plain_parameter_type(
@@ -243,9 +243,9 @@
     LAZR_WEBSERVICE_EXPORTED)['return_type'].schema = IBranchSubscription
 IBranch['subscriptions'].value_type.schema = IBranchSubscription
 IBranch['unlinkBug'].queryTaggedValue(
-    LAZR_WEBSERVICE_EXPORTED)['params']['bug'].schema= IBug
+    LAZR_WEBSERVICE_EXPORTED)['params']['bug'].schema = IBug
 IBranch['unlinkSpecification'].queryTaggedValue(
-    LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema= ISpecification
+    LAZR_WEBSERVICE_EXPORTED)['params']['spec'].schema = ISpecification
 
 patch_entry_return_type(IBranch, '_createMergeProposal', IBranchMergeProposal)
 patch_plain_parameter_type(
@@ -426,6 +426,14 @@
     IArchive, 'getUploadersForPackageset', 'packageset', IPackageset)
 patch_plain_parameter_type(
     IArchive, 'deletePackagesetUploader', 'packageset', IPackageset)
+patch_plain_parameter_type(
+    IArchive, 'removeArchiveDependency', 'dependency', IArchive)
+patch_plain_parameter_type(
+    IArchive, '_addArchiveDependency', 'dependency', IArchive)
+patch_choice_parameter_type(
+    IArchive, '_addArchiveDependency', 'pocket', PackagePublishingPocket)
+patch_entry_return_type(
+    IArchive, '_addArchiveDependency', IArchiveDependency)
 
 
 # IBuildFarmJob

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py	2011-05-27 21:12:25 +0000
+++ lib/lp/soyuz/browser/archive.py	2011-06-02 14:48:33 +0000
@@ -33,7 +33,6 @@
     datetime,
     timedelta,
     )
-from urlparse import urlparse
 
 import pytz
 from sqlobject import SQLObjectNotFound
@@ -142,6 +141,7 @@
     IArchiveEditDependenciesForm,
     IArchiveSet,
     NoSuchPPA,
+    validate_external_dependencies,
     )
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
 from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
@@ -2146,7 +2146,7 @@
         # Check the external_dependencies field.
         ext_deps = data.get('external_dependencies')
         if ext_deps is not None:
-            errors = self.validate_external_dependencies(ext_deps)
+            errors = validate_external_dependencies(ext_deps)
             if len(errors) != 0:
                 error_text = "\n".join(errors)
                 self.setFieldError('external_dependencies', error_text)
@@ -2156,31 +2156,6 @@
                 'commercial',
                 'Can only set commericial for private archives.')
 
-    def validate_external_dependencies(self, ext_deps):
-        """Validate the external_dependencies field.
-
-        :param ext_deps: The dependencies form field to check.
-        """
-        errors = []
-        # The field can consist of multiple entries separated by
-        # newlines, so process each in turn.
-        for dep in ext_deps.splitlines():
-            try:
-                deb, url, suite, components = dep.split(" ", 3)
-            except ValueError:
-                errors.append(
-                    "'%s' is not a complete and valid sources.list entry"
-                        % dep)
-                continue
-
-            if deb != "deb":
-                errors.append("%s: Must start with 'deb'" % dep)
-            url_components = urlparse(url)
-            if not url_components[0] or not url_components[1]:
-                errors.append("%s: Invalid URL" % dep)
-
-        return errors
-
     @property
     def owner_is_private_team(self):
         """Is the owner a private team?

=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
--- lib/lp/soyuz/browser/tests/test_archive_webservice.py	2010-10-26 15:47:24 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py	2011-06-02 14:48:33 +0000
@@ -1,19 +1,30 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 import unittest
 
-from lazr.restfulclient.errors import HTTPError
+from lazr.restfulclient.errors import (
+    BadRequest,
+    HTTPError,
+    Unauthorized as LRUnauthorized,
+)
+from testtools import ExpectedException
+import transaction
+from zope.component import getUtility
 
 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
 from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.soyuz.enums import ArchivePurpose
 from lp.testing import (
     celebrity_logged_in,
     launchpadlib_for,
+    person_logged_in,
     TestCaseWithFactory,
+    WebServiceTestCase,
     )
 
 
@@ -67,5 +78,111 @@
             "in the 'DEVELOPMENT' state.", e.content)
 
 
+class TestExternalDependencies(WebServiceTestCase):
+
+    def test_external_dependencies_random_user(self):
+        """Normal users can look but not touch."""
+        archive = self.factory.makeArchive()
+        transaction.commit()
+        ws_archive = self.wsObject(archive)
+        self.assertIs(None, ws_archive.external_dependencies)
+        ws_archive.external_dependencies = "random"
+        with ExpectedException(LRUnauthorized, '.*'):
+            ws_archive.lp_save()
+
+    def test_external_dependencies_owner(self):
+        """Normal archive owners can look but not touch."""
+        archive = self.factory.makeArchive()
+        transaction.commit()
+        ws_archive = self.wsObject(archive, archive.owner)
+        self.assertIs(None, ws_archive.external_dependencies)
+        ws_archive.external_dependencies = "random"
+        with ExpectedException(LRUnauthorized, '.*'):
+            ws_archive.lp_save()
+
+    def test_external_dependencies_commercial_owner_invalid(self):
+        """Commercial admins can look and touch."""
+        commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+        owner = self.factory.makePerson(member_of=[commercial])
+        archive = self.factory.makeArchive(owner=owner)
+        transaction.commit()
+        ws_archive = self.wsObject(archive, archive.owner)
+        self.assertIs(None, ws_archive.external_dependencies)
+        ws_archive.external_dependencies = "random"
+        regex = '(\n|.)*Invalid external dependencies(\n|.)*'
+        with ExpectedException(BadRequest, regex):
+            ws_archive.lp_save()
+
+    def test_external_dependencies_commercial_owner_valid(self):
+        """Commercial admins can look and touch."""
+        commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+        owner = self.factory.makePerson(member_of=[commercial])
+        archive = self.factory.makeArchive(owner=owner)
+        transaction.commit()
+        ws_archive = self.wsObject(archive, archive.owner)
+        self.assertIs(None, ws_archive.external_dependencies)
+        ws_archive.external_dependencies = (
+            "deb http://example.org suite components")
+        ws_archive.lp_save()
+
+
+class TestArchiveDependencies(WebServiceTestCase):
+
+    def test_addArchiveDependency_random_user(self):
+        """Normal users cannot add archive dependencies."""
+        archive = self.factory.makeArchive()
+        dependency = self.factory.makeArchive()
+        transaction.commit()
+        ws_archive = self.wsObject(archive)
+        ws_dependency = self.wsObject(dependency)
+        self.assertContentEqual([], ws_archive.dependencies)
+        failure_regex = '(.|\n)*addArchiveDependency.*launchpad.Edit(.|\n)*'
+        with ExpectedException(LRUnauthorized, failure_regex):
+            dependency = ws_archive.addArchiveDependency(
+                dependency=ws_dependency, pocket='Release', component='main')
+
+    def test_addArchiveDependency_owner(self):
+        """Normal users cannot add archive dependencies."""
+        archive = self.factory.makeArchive()
+        dependency = self.factory.makeArchive()
+        transaction.commit()
+        ws_archive = self.wsObject(archive, archive.owner)
+        ws_dependency = self.wsObject(dependency)
+        self.assertContentEqual([], ws_archive.dependencies)
+        with ExpectedException(BadRequest, '(.|\n)*asdf(.|\n)*'):
+            ws_archive.addArchiveDependency(
+                dependency=ws_dependency, pocket='Release', component='asdf')
+        dependency = ws_archive.addArchiveDependency(
+            dependency=ws_dependency, pocket='Release', component='main')
+        self.assertContentEqual([dependency], ws_archive.dependencies)
+
+    def test_removeArchiveDependency_random_user(self):
+        """Normal users can remove archive dependencies."""
+        archive = self.factory.makeArchive()
+        dependency = self.factory.makeArchive()
+        with person_logged_in(archive.owner):
+            archive.addArchiveDependency(
+                dependency, PackagePublishingPocket.RELEASE)
+        transaction.commit()
+        ws_archive = self.wsObject(archive)
+        ws_dependency = self.wsObject(dependency)
+        failure_regex = '(.|\n)*remove.*Dependency.*launchpad.Edit(.|\n)*'
+        with ExpectedException(LRUnauthorized, failure_regex):
+            ws_archive.removeArchiveDependency(dependency=ws_dependency)
+
+    def test_removeArchiveDependency_owner(self):
+        """Normal users can remove archive dependencies."""
+        archive = self.factory.makeArchive()
+        dependency = self.factory.makeArchive()
+        with person_logged_in(archive.owner):
+            archive.addArchiveDependency(
+                dependency, PackagePublishingPocket.RELEASE)
+        transaction.commit()
+        ws_archive = self.wsObject(archive, archive.owner)
+        ws_dependency = self.wsObject(dependency)
+        ws_archive.removeArchiveDependency(dependency=ws_dependency)
+        self.assertContentEqual([], ws_archive.dependencies)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2011-05-19 04:50:33 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2011-06-02 14:48:33 +0000
@@ -32,6 +32,7 @@
     'IDistributionArchive',
     'InsufficientUploadRights',
     'InvalidComponent',
+    'InvalidExternalDependencies',
     'InvalidPocketForPartnerArchive',
     'InvalidPocketForPPA',
     'IPPA',
@@ -43,8 +44,12 @@
     'PocketNotFound',
     'VersionRequiresName',
     'default_name_by_purpose',
+    'validate_external_dependencies',
     ]
 
+
+from urlparse import urlparse
+
 from lazr.enum import DBEnumeratedType
 from lazr.restful.declarations import (
     call_with,
@@ -54,6 +59,7 @@
     export_read_operation,
     export_write_operation,
     exported,
+    operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
     operation_returns_entry,
@@ -114,59 +120,59 @@
 
 class CannotCopy(Exception):
     """Exception raised when a copy cannot be performed."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class CannotSwitchPrivacy(Exception):
     """Raised when switching the privacy of an archive that has
     publishing records."""
-    webservice_error(400) # Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class PocketNotFound(Exception):
     """Invalid pocket."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class DistroSeriesNotFound(Exception):
     """Invalid distroseries."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class AlreadySubscribed(Exception):
     """Raised when creating a subscription for a subscribed person."""
-    webservice_error(400) # Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class ArchiveNotPrivate(Exception):
     """Raised when creating an archive subscription for a public archive."""
-    webservice_error(400) # Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class NoTokensForTeams(Exception):
     """Raised when creating a token for a team, rather than a person."""
-    webservice_error(400) # Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class ComponentNotFound(Exception):
     """Invalid source name."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class InvalidComponent(Exception):
     """Invalid component name."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class NoSuchPPA(NameLookupFailed):
     """Raised when we try to look up an PPA that doesn't exist."""
-    webservice_error(400) #Bad request.
+    webservice_error(400)  # Bad request.
     _message_prefix = "No such ppa"
 
 
 class VersionRequiresName(Exception):
     """Raised on some queries when version is specified but name is not."""
-    webservice_error(400) # Bad request.
+    webservice_error(400)  # Bad request.
 
 
 class CannotRestrictArchitectures(Exception):
@@ -175,7 +181,7 @@
 
 class CannotUploadToArchive(Exception):
     """A reason for not being able to upload to an archive."""
-    webservice_error(403) # Forbidden.
+    webservice_error(403)  # Forbidden.
 
     _fmt = '%(person)s has no upload rights to %(archive)s.'
 
@@ -192,7 +198,7 @@
 
 class CannotUploadToPocket(Exception):
     """Returned when a pocket is closed for uploads."""
-    webservice_error(403) # Forbidden.
+    webservice_error(403)  # Forbidden.
 
     def __init__(self, distroseries, pocket):
         Exception.__init__(self,
@@ -248,6 +254,17 @@
         CannotUploadToArchive.__init__(self, archive_name=archive_name)
 
 
+class InvalidExternalDependencies(Exception):
+    """Tried to set external dependencies to an invalid value."""
+
+    webservice_error(400)  # Bad request.
+
+    def __init__(self, errors):
+        error_msg = 'Invalid external dependencies:\n%s\n' % '\n'.join(errors)
+        Exception.__init__(self, error_msg)
+        self.errors = errors
+
+
 class IArchivePublic(IHasOwner, IPrivacy):
     """An Archive interface for publicly available operations."""
     id = Attribute("The archive ID.")
@@ -332,7 +349,7 @@
 
     distribution = exported(
         Reference(
-            Interface, # Redefined to IDistribution later.
+            Interface,  # Redefined to IDistribution later.
             title=_("The distribution that uses or is used by this "
                     "archive.")))
 
@@ -412,9 +429,9 @@
             "A delta to apply to all build scores for the archive. Builds "
             "with a higher score will build sooner."))
 
-    external_dependencies = Text(
-        title=_("External dependencies"), required=False, readonly=False,
-        description=_(
+    external_dependencies = exported(
+        Text(title=_("External dependencies"), required=False,
+        readonly=False, description=_(
             "Newline-separated list of repositories to be used to retrieve "
             "any external build dependencies when building packages in the "
             "archive, in the format:\n"
@@ -422,7 +439,7 @@
                 "[components]\n"
             "The series variable is replaced with the series name of the "
             "context build.\n"
-            "NOTE: This is for migration of OEM PPAs only!"))
+            "NOTE: This is for migration of OEM PPAs only!")))
 
     enabled_restricted_families = CollectionField(
             title=_("Enabled restricted families"),
@@ -520,27 +537,6 @@
             records.
         """
 
-    def removeArchiveDependency(dependency):
-        """Remove the `IArchiveDependency` record for the given dependency.
-
-        :param dependency: is an `IArchive` object.
-        """
-
-    def addArchiveDependency(dependency, pocket, component=None):
-        """Record an archive dependency record for the context archive.
-
-        :param dependency: is an `IArchive` object.
-        :param pocket: is an `PackagePublishingPocket` enum.
-        :param component: is an optional `IComponent` object, if not given
-            the archive dependency will be tied to the component used
-            for a corresponding source in primary archive.
-
-        :raise: `ArchiveDependencyError` if given 'dependency' does not fit
-            the context archive.
-        :return: a `IArchiveDependency` object targeted to the context
-            `IArchive` requiring 'dependency' `IArchive`.
-        """
-
     def getPermissions(person, item, perm_type):
         """Get the `IArchivePermission` record with the supplied details.
 
@@ -892,7 +888,8 @@
         :return: True if the person is allowed to upload the source package.
         """
 
-    num_pkgs_building = Attribute("Tuple of packages building and waiting to build")
+    num_pkgs_building = Attribute(
+        "Tuple of packages building and waiting to build")
 
     def getSourcePackageReleases(build_status=None):
         """Return the releases for this archive.
@@ -944,7 +941,8 @@
     dependencies = exported(
         CollectionField(
             title=_("Archive dependencies recorded for this archive."),
-            value_type=Reference(schema=Interface), #Really IArchiveDependency
+            value_type=Reference(schema=Interface),
+            # Really IArchiveDependency
             readonly=True))
 
     description = exported(
@@ -1111,8 +1109,8 @@
         """
 
     @operation_parameters(
-        dependency=Reference(schema=Interface)) #Really IArchive. See below.
-    @operation_returns_entry(schema=Interface) #Really IArchiveDependency.
+        dependency=Reference(schema=Interface))  # Really IArchive. See below.
+    @operation_returns_entry(schema=Interface)  # Really IArchiveDependency.
     @export_read_operation()
     def getArchiveDependency(dependency):
         """Return the `IArchiveDependency` object for the given dependency.
@@ -1233,7 +1231,8 @@
         source_names=List(
             title=_("Source package names"),
             value_type=TextLine()),
-        from_archive=Reference(schema=Interface), #Really IArchive, see below
+        from_archive=Reference(schema=Interface),
+        #Really IArchive, see below
         to_pocket=TextLine(title=_("Pocket name")),
         to_series=TextLine(title=_("Distroseries name"), required=False),
         include_binaries=Bool(
@@ -1275,7 +1274,8 @@
     @operation_parameters(
         source_name=TextLine(title=_("Source package name")),
         version=TextLine(title=_("Version")),
-        from_archive=Reference(schema=Interface), #Really IArchive, see below
+        from_archive=Reference(schema=Interface),
+        # Really IArchive, see below
         to_pocket=TextLine(title=_("Pocket name")),
         to_series=TextLine(title=_("Distroseries name"), required=False),
         include_binaries=Bool(
@@ -1314,7 +1314,7 @@
 
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
-        subscriber = PublicPersonChoice(
+        subscriber=PublicPersonChoice(
             title=_("Subscriber"),
             required=True,
             vocabulary='ValidPersonOrTeam',
@@ -1458,6 +1458,61 @@
         processed.
         """
 
+    def addArchiveDependency(dependency, pocket, component=None):
+        """Record an archive dependency record for the context archive.
+
+        :param dependency: is an `IArchive` object.
+        :param pocket: is an `PackagePublishingPocket` enum.
+        :param component: is an optional `IComponent` object, if not given
+            the archive dependency will be tied to the component used
+            for a corresponding source in primary archive.
+
+        :raise: `ArchiveDependencyError` if given 'dependency' does not fit
+            the context archive.
+        :return: a `IArchiveDependency` object targeted to the context
+            `IArchive` requiring 'dependency' `IArchive`.
+        """
+
+    @operation_parameters(
+        dependency=Reference(schema=Interface, required=True),
+        #  Really IArchive
+        pocket=Choice(
+            title=_("Pocket"),
+            description=_("The pocket into which this entry is published"),
+            # Really PackagePublishingPocket.
+            vocabulary=DBEnumeratedType,
+            required=True),
+        component=TextLine(title=_("Component"), required=False),
+        )
+    @export_operation_as('addArchiveDependency')
+    @export_factory_operation(Interface, [])  # Really IArchiveDependency
+    @operation_for_version('devel')
+    def _addArchiveDependency(dependency, pocket, component=None):
+        """Record an archive dependency record for the context archive.
+
+        :param dependency: is an `IArchive` object.
+        :param pocket: is an `PackagePublishingPocket` enum.
+        :param component: is the name of a component.  If not given,
+            the archive dependency will be tied to the component used
+            for a corresponding source in primary archive.
+
+        :raise: `ArchiveDependencyError` if given 'dependency' does not fit
+            the context archive.
+        :return: a `IArchiveDependency` object targeted to the context
+            `IArchive` requiring 'dependency' `IArchive`.
+        """
+    @operation_parameters(
+        dependency=Reference(schema=Interface, required=True),
+        # Really IArchive
+    )
+    @export_write_operation()
+    @operation_for_version('devel')
+    def removeArchiveDependency(dependency):
+        """Remove the `IArchiveDependency` record for the given dependency.
+
+        :param dependency: is an `IArchive` object.
+        """
+
 
 class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView):
     """Main Archive interface."""
@@ -1690,3 +1745,29 @@
     )
 
 # Circular dependency issues fixed in _schema_circular_imports.py
+
+
+def validate_external_dependencies(ext_deps):
+    """Validate the external_dependencies field.
+
+    :param ext_deps: The dependencies form field to check.
+    """
+    errors = []
+    # The field can consist of multiple entries separated by
+    # newlines, so process each in turn.
+    for dep in ext_deps.splitlines():
+        try:
+            deb, url, suite, components = dep.split(" ", 3)
+        except ValueError:
+            errors.append(
+                "'%s' is not a complete and valid sources.list entry"
+                    % dep)
+            continue
+
+        if deb != "deb":
+            errors.append("%s: Must start with 'deb'" % dep)
+        url_components = urlparse(url)
+        if not url_components[0] or not url_components[1]:
+            errors.append("%s: Invalid URL" % dep)
+
+    return errors

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2011-05-27 21:12:25 +0000
+++ lib/lp/soyuz/model/archive.py	2011-06-02 14:48:33 +0000
@@ -125,6 +125,7 @@
     CannotSwitchPrivacy,
     CannotUploadToPocket,
     CannotUploadToPPA,
+    ComponentNotFound,
     default_name_by_purpose,
     DistroSeriesNotFound,
     FULL_COMPONENT_SUPPORT,
@@ -133,6 +134,7 @@
     IDistributionArchive,
     InsufficientUploadRights,
     InvalidComponent,
+    InvalidExternalDependencies,
     InvalidPocketForPartnerArchive,
     InvalidPocketForPPA,
     IPPA,
@@ -143,6 +145,7 @@
     NoTokensForTeams,
     PocketNotFound,
     VersionRequiresName,
+    validate_external_dependencies,
     )
 from lp.soyuz.interfaces.archivearch import IArchiveArchSet
 from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
@@ -197,6 +200,14 @@
 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
 
+def storm_validate_external_dependencies(archive, attr, value):
+    assert attr == 'external_dependencies'
+    errors = validate_external_dependencies(value)
+    if len(errors) > 0:
+        raise InvalidExternalDependencies(errors)
+    return value
+
+
 class Archive(SQLBase):
     implements(IArchive, IHasOwner, IHasBuildRecords)
     _table = 'Archive'
@@ -306,7 +317,8 @@
     # Launchpad and should be re-examined in October 2010 to see if it
     # is still relevant.
     external_dependencies = StringCol(
-        dbName='external_dependencies', notNull=False, default=None)
+        dbName='external_dependencies', notNull=False, default=None,
+        storm_validator=storm_validate_external_dependencies)
 
     commercial = BoolCol(
         dbName='commercial', notNull=True, default=False)
@@ -483,7 +495,7 @@
 
         if name is not None:
             if exact_match:
-                storm_clauses.append(SourcePackageName.name==name)
+                storm_clauses.append(SourcePackageName.name == name)
             else:
                 clauses.append(
                     "SourcePackageName.name LIKE '%%%%' || %s || '%%%%'"
@@ -494,7 +506,7 @@
                 raise VersionRequiresName(
                     "The 'version' parameter can be used only together with"
                     " the 'name' parameter.")
-            storm_clauses.append(SourcePackageRelease.version==version)
+            storm_clauses.append(SourcePackageRelease.version == version)
         else:
             orderBy.insert(1, Desc(SourcePackageRelease.version))
 
@@ -514,7 +526,7 @@
 
         if pocket is not None:
             storm_clauses.append(
-                SourcePackagePublishingHistory.pocket==pocket)
+                SourcePackagePublishingHistory.pocket == pocket)
 
         if created_since_date is not None:
             clauses.append(
@@ -529,6 +541,7 @@
             *orderBy)
         if not eager_load:
             return resultset
+
         # Its not clear that this eager load is necessary or sufficient, it
         # replaces a prejoin that had pathological query plans.
         def eager_load(rows):
@@ -609,7 +622,7 @@
         clauseTables = ['SourcePackageRelease', 'SourcePackageName']
 
         order_const = "SourcePackageRelease.version"
-        desc_version_order = SQLConstant(order_const+" DESC")
+        desc_version_order = SQLConstant(order_const + " DESC")
         orderBy = ['SourcePackageName.name', desc_version_order,
                    '-SourcePackagePublishingHistory.id']
 
@@ -970,6 +983,15 @@
             archive=self, dependency=dependency, pocket=pocket,
             component=component)
 
+    def _addArchiveDependency(self, dependency, pocket, component=None):
+        """See `IArchive`."""
+        if isinstance(component, basestring):
+            try:
+                component = getUtility(IComponentSet)[component]
+            except NotFoundError as e:
+                raise ComponentNotFound(e)
+        return self.addArchiveDependency(dependency, pocket, component)
+
     def getPermissions(self, user, item, perm_type):
         """See `IArchive`."""
         permission_set = getUtility(IArchivePermissionSet)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-05-28 04:09:11 +0000
+++ lib/lp/testing/factory.py	2011-06-02 14:48:33 +0000
@@ -593,7 +593,7 @@
         self, email=None, name=None, password=None,
         email_address_status=None, hide_email_addresses=False,
         displayname=None, time_zone=None, latitude=None, longitude=None,
-        selfgenerated_bugnotifications=False):
+        selfgenerated_bugnotifications=False, member_of=()):
         """Create and return a new, arbitrary Person.
 
         :param email: The email address for the new person.
@@ -658,6 +658,10 @@
 
         self.makeOpenIdIdentifier(person.account)
 
+        for team in member_of:
+            with person_logged_in(team.teamowner):
+                team.addMember(person, team.teamowner)
+
         # Ensure updated ValidPersonCache
         flush_database_updates()
         return person